En el marco del Primer Desafío Internacional de la Red Latinoamericana de Ciencia de Datos, este análisis tiene como propósito abordar un problema práctico del ámbito logístico. El proyecto promueve la colaboración entre estudiantes de diversas universidades latinoamericanas, fomentando el trabajo en equipo y la toma de decisiones basadas en datos reales.
El objeto de estudio es un conjunto de datos proporcionado por iFlow, una empresa argentina especializada en logística integral, con operaciones tanto nacionales como internacionales dentro del MERCOSUR. iFlow se dedica a la gestión y co-gerencia de cadenas de abastecimiento para sus clientes, buscando optimizar procesos y mejorar la eficiencia operativa.
Este análisis tiene como objetivo:
Comprender y describir las principales características del conjunto de datos, que incluye registros de entregas realizadas en un período de tres meses.
Identificar patrones y tendencias que permitan obtener insights relevantes sobre las operaciones de iFlow.
Detectar posibles inconsistencias o errores en la base de datos, propias de un entorno operativo real, para evaluarlas e integrarlas al análisis.
2 Limpieza de datos.
La primera etapa de este análisis consistió en la limpieza de datos. Este proceso de limpieza fue clave para preparar los datos para un análisis exploratorio robusto y la generación de insights confiables sobre la operación logística de iFlow.
En primer lugar realizamos algunos cambios para facilitar el trabajo con los datos.
Tratar Columnas innecesarias
La columna InicioVisitaPlanificado y FinVisitaPlanificado contienen los mismos valores por lo que las unificamos en una nueva columna.
Formatear correctamente las variables
Renombrar las columnas por nombres intuitivos.
Con estos cambios realizados pasamos a modificaciones y arreglos necesarios para un análisis correcto de los datos.
Eliminación de filas duplicadas.
Arreglo de coordenadas faltantes en algunas entregas.
El dato de coordenadas en algunas filas estaba vacio o indicaba “0”. En algunos de estos casos pudimos rellenar estas coordenadas con datos existentes del domicilio (21 filas). En caso de que esto no sea posible las filas fueron eliminadas (19 filas) y no serán tomadas en cuenta para el análisis.
Por último creamos algunas nuevas columnas para distintos análisis. Entre estas algunas columnas para facilitar la interacción con fechas y horarios de entregas.
3 Vista general.
En esta sección ofrecemos una visión superficial de los datos, brindando un panorama inicial que permite familiarizarnos con su estructura y contenido.
Se registraron 27.419 entregas de dos clientes distintos: 16,545 del cliente 20 y 10.874 del cliente 70.
Todas estas entregas fueron realizadas entre el 3 de mayo y 8 de agosto de 2024, con un promedio de 9.005 entregas por mes
Este gráfico de barras muestra la cantidad de entregas realizadas cada mes, separadas por cliente. Las barras están agrupadas por mes y se distinguen por colores para cada cliente.
La cantidad de entregas es constante entre mayo y julio, con algunas variaciones entre los clientes. Esto indica un comportamiento predecible en la demanda mensual. Planificar recursos en función de estos patrones puede mejorar la eficiencia operativa.
En este gráfico se visualiza la cantidad total de entregas realizadas por mes, sin importar el cliente.
n patrón estable en el número de entregas permite predecir la demanda mensual. Las caídas pueden ser analizadas más adelante para identificar feriados o problemas en la operación.
3.1 ¿Cuales son los horarios de entrega?
Este gráfico de calor muestra la cantidad de entregas realizadas según la hora del día y el día de la semana. Cada celda del gráfico representa un cruce entre una hora y un día, con colores más intensos indicando una mayor cantidad de entregas.
Mayor actividad: El miércoles alrededor de las 15:00 es el momento con más entregas. Patrón semanal: Hay menos actividad los fines de semana, especialmente los domingos, donde casi no se registran entregas.
Vemos que el domingo hay solo 3 entregas. Esto podría deberse a un error por lo que las revisamos.
Orden
Localidad
Fecha y hora de entrega
81943
CAPITAL FEDERAL
2024-07-21 23:51:00
100968
CAPITAL FEDERAL
2024-07-21 23:51:00
100968
CAPITAL FEDERAL
2024-07-21 23:51:00
Al revisar estas entregas encontramos que las tres fueron entregadas a las 23:51. Esto podría deberse a un error en la carga de datos o a entregas especiales. Al ser las últimas entregas del día puede deberse a un cierre automático del sistema despues de haber dejado entregas inciadas el día anterior sin marcar su finalización.
Si bien este parece ser un caso puntual existen dos grandes problemas respecto a los horarios y duración de las entregas. Estos problemas son.
Duración de las entregas: Muchas entregas tienen el mismo horario de inicio y fin. Esto puede deberse a errores en la carga de datos o a un sistema de carga poco eficiente.
Entregas consecutivas: Muchas entregas consecutivas fueron cargadas con el horario de la entrega anterior. Esto dificulta la identificación de rutas, estimación de tiempos muertos y duraciones reales entre entregas.
3.2 Errores de carga en horarios de entrega.
Notamos que en las entregas muy cercanas geograficamente, en la misma cuadra, suelen tener el mismo horario de finalización. Esto se puede deber a que los operarios olvidan hacer la carga individual o consideran mas rapido completar ambas entregas antes de registrarlo en el sistema.
Junto con los errores de carga en los horarios de entrega este puede ser un segundo indicador de que el sistema de carga puede ser mejorado para no recolectar datos erroneos en el futuro.
Horarios cargados de forma incorrecta podrian causar:
Mala estimación sobre tiempos muertos.
Dificulta optimizar los procesos de entrega.
Perjudica la proyección de horarios de entregas o ventanas horarias.
Algunas sugerencias e ideas para mejorar esto incluyen:
Mejoras de la interfaz en el sistema de carga para facilitar y fomentar su uso.
Implementación de un sistema de validación de los datos para evitar duplicados.
Desarrollo y uso de hardware específico para la carga de datos.
¿Cuál es el tiempo promedio entre el inicio y fin de las visitas de entrega?
Bien cargados 17142 vs mal cargados 10255 mal. Pero encontramos que son incluso más. Una forma de solucionarlo podría haber sido ordenar las entregas en orden cronologico e intentar estimar la duración real segun el tiempo entre las entregas y la distancia entre ellas pero al intentar esto encontramos que muchas entregas consecutivas arrastran errores. Dificultando la identificación de rutas, estimación de tiempos muertos y duraciones reales entre entregas.
Como referencia, el día 2024-06-27 16:06:00 hay 23 entregas graficadas el mismo día.
Se registraron 12,799 entregas con horarios incorrectos, lo que representa el 46.67% de los datos cargados de forma errónea.
Con casi la mitad de los datos poco confiables, cualquier análisis sobre la eficiencia operativa pierde precisión. Esto puede generar errores importantes al evaluar la duración de las entregas, los periodos de actividad y las rutas utilizadas.
Corregir la recopilación de estos datos es una oportunidad clave para mejorar el crecimiento de la empresa, optimizar las ventanas de entrega, identificar cuellos de botella reales y facilitar una toma de decisiones más precisa y efectiva.
“Lo que no se mide, no se puede mejorar.” - Peter F. Drucker
A continuación, se presentan posibles causas y oportunidades para resolver esta situación.
Manual de uso y capacitación sobre el sistema UNIGIS
Capacitar mejor al personal con mayor cantidad de recursos, claridad en los instructivos y videos demostrando el uso correcto del sistema videos demostrando el uso correcto del sistema mejoraría la precision de la carga de los datos en el futuro.
Creación de una interfaz personalizada para Unigis.
El error de carga de horarios iguales en entregas consecutivas puede deberse principalmente a la dificultad de uso del sistema o poca practicidad del mismo por la que los transportistas podrían saltear los pasos del instructivo.
Si migrar a un nuevo sistema más moderno es una alternativa muy costosa podrían considerar hacer una inversión en desarrollo frontend para, utilizando la API del sistema actual, puedan tener una interfaz más amena a los transportistas.
Referencia del Uso de la API cloud de Unigis
El desarrollo de una interfaz personalizada para interactuar con su sistema actual podría ser una inversión considerable pero economica contrastando con la posibilidad de un desarrollo personalizado o la migración a un nuevo sistema.
Algunas consideraciones:
Inversión en equipo e investigación UX para asegurar el uso intuitivo de los transportistas. Es importante entender como es el uso del sistema en la practica.
Migrar a un sistema más moderno o diseñar uno a medida para sus necesidades.
Puede ser la opción mas costosa.
Como conclusión mejorar la capacitación del personal, proporcionando más recursos, instructivos claros y videos demostrativos sobre el uso correcto del sistema, ayudaría a aumentar la precisión en la carga de datos. Además, la creación de una interfaz personalizada para Unigis podría resolver errores comunes, como los horarios idénticos en entregas consecutivas, que probablemente ocurren por la complejidad o poca practicidad del sistema, lo que lleva a los transportistas a omitir pasos. Si migrar a un sistema más moderno resulta demasiado costoso, una opción sería invertir en el desarrollo de un frontend más amigable, utilizando la API del sistema actual, para facilitar su uso y mejorar la experiencia de los transportistas.
3.3 Información de bultos y peso.
En promedio, cada entrega contiene:
28.40 unidades
5.75 bultos
41.15 kg de peso
El peso elevado por pedido sugiere la presencia de productos voluminosos o de alto consumo, lo que puede aumentar los costos de transporte debido a la mayor carga. Esto también tiene implicaciones importantes para la planificación de rutas, ya que es necesario optimizar el uso de los vehículos para evitar sobrecargas y cumplir con los límites de capacidad. Además, la gestión eficiente de estos pedidos requiere ajustar los tiempos de entrega, garantizar la disponibilidad de unidades adecuadas y considerar posibles restricciones en ciertas rutas debido al peso total transportado.
Como podemos ver este mapa de entregas de la ciudad de Buenos Aires muestra visualmente la dispersión de las entregas en la ciudad. La variedad de colores permite ver qué clientes se agrupan en ciertas áreas. Si ciertos clientes concentran entregas en una región específica, eso puede indicar una demanda localizada o preferencia regional de ciertos productos.
En este mapa las áreas con mayor densidad de entregas resaltan visualmente. Estas zonas de alta actividad pueden representar zonas comerciales o residenciales clave. Esto es útil para optimizar las rutas de entrega y asignar más recursos a zonas con alta demanda.
En base a este mapa se permite ver cómo se distribuyen las entregas a nivel de barrio. Los barrios con mayor intensidad de color son aquellos con más entregas, lo que indica la importancia de estos barrios en el volumen de pedidos.
Similar al mapa por barrio, pero agrupando entregas por comunas. Las comunas que presentan más entregas destacan como puntos de interés para analizar el impacto logístico y la demanda concentrada.
Los puntos en el mapa de Buenos Aires representan ubicaciones de entregas, y las áreas con mayor densidad de entregas aparecen con colores más intensos, permitiendo identificar visualmente las zonas con más actividad de entregas.
`summarise()` has grouped output by 'latitud'. You can override using the
`.groups` argument.
¿Cuántas entregas se hicieron fuera del tiempo esperado o planificado?
`summarise()` has grouped output by 'mes'. You can override using the `.groups`
argument.
Este gráfico permite la visualización de la cantidad de entregas por mes, clasificadas en “Tarde” y “Temprano o a Tiempo”.
¿Qué clientes generan más volumen de entregas y cuáles presentan más problemas o irregularidades?
Este mapa permite la visualización de un mapa interactivo que muestra las entregas realizadas, clasificado por cliente, en Buenos Aires.
3.5 Conclusiones
4 Segmentación y patrones en entregas.
El gráfico de barras muestra cuántas entregas ha recibido cada cliente. Cada barra representa a un cliente, y su altura indica el total de entregas realizadas. Así, es fácil comparar rápidamente qué clientes tienen más o menos entregas.
Este gráfico de líneas muestra la cantidad de entregas realizadas a dos clientes específicos (identificados como “20” y “70”) a lo largo de los días de la semana. Cada línea representa a un cliente, permitiendo analizar cómo varían sus entregas día a día.
Podemos observar que el cliente 20 tiene un pico de entregas el miércoles y una disminución notable el jueves, con una ligera recuperación hacia el final de la semana. Por otro lado, el cliente 70 mantiene un comportamiento más estable a lo largo de los días, con pequeñas variaciones pero sin grandes picos o caídas. Esta visualización facilita comparar los patrones de entrega entre ambos clientes y puede ayudar a identificar días de mayor o menor actividad para cada uno.
`summarise()` has grouped output by 'cliente'. You can override using the
`.groups` argument.
Este gráfico de lineas permite la visualización de la cantidad de entregas realizadas por cliente a lo largo de los días de la semana, excluyendo el domingo. Utiliza líneas para representar la evolución del número de entregas para cada cliente, con puntos destacados en cada día.
Este gráfico permite la visualización de la distribución de la cantidad de entregas por domicilio, mostrando un histograma que representa la frecuencia de entregas para diferentes valores.
Este gráfico de barra permite la visualización de la cantidad de domicilios que han recibido más de una entrega. Utiliza barras para representar el número total de domicilios, distinguiendo entre aquellos que tienen mas de una entrega y los que no.
Este histograma muestra la distribución de la cantidad de entregas por domicilio, permitiendo identificar cuántas veces se realizaron múltiples entregas en un mismo lugar durante un periodo de tres meses. En el gráfico, cada barra representa la frecuencia de domicilios que recibieron cierta cantidad de entregas.
Podemos observar que la mayoría de los domicilios recibieron dos entregas, lo que se refleja en la mayor altura de esa barra. Sin embargo, también se registran casos con más entregas, aunque en menor frecuencia, disminuyendo progresivamente a medida que aumenta el número de entregas por domicilio. En algunos casos excepcionales, se registraron más de X entregas en el mismo domicilio, lo que sugiere que ciertos clientes tienen una mayor recurrencia en los pedidos. Este análisis permite identificar tanto los patrones de entrega más comunes como los casos atípicos con entregas inusualmente altas.
El análisis mostrado utiliza el algoritmo de K-means para agrupar las entregas por su localidad y cercanía geográfica en la ciudad de Buenos Aires. El propósito de aplicar este tipo de clustering es identificar zonas de entrega de manera más eficiente y precisa, evitando segmentaciones arbitrarias como barrios o comunas, que podrían no reflejar de manera realista los patrones de distribución.
Este gráfico nos ayuda a comprender el número de clusters utilizados en el análisis de K-means como la distribución geográfica de los mismos en el área de Buenos Aires.
Para determinar la cantidad óptima de clusters (K) que mejor agrupe los puntos de entrega, se aplicó el método del codo. Este método mide la suma de cuadrados dentro del cluster (WSS), que indica cuán compactos son los grupos formados. El gráfico muestra que a medida que aumenta el número de clusters, el WSS disminuye, lo que significa que los grupos son cada vez más específicos. Sin embargo, a partir de K = 4, la reducción adicional en WSS es menor, formando un “codo”. Por eso, seleccionamos 4 clusters como el número óptimo para este análisis.
4.0.1 Interpretación del Mapa y los Clusters
En el mapa, los puntos de entrega están agrupados en cuatro clusters, cada uno representado por un color diferente. Esta división refleja de manera efectiva las zonas donde se concentran las entregas:
Zona Norte: Agrupa entregas hacia localidades como Vicente López y San Isidro.
Zona Sur: Incluye áreas como Lanús, Avellaneda y alrededores.
Zona Oeste: Abarca lugares como Morón y La Matanza.
Zona Este: Se concentra cerca de la Ciudad Autónoma de Buenos Aires, incluyendo zonas cercanas al Río de la Plata.
Estos clusters permiten visualizar de forma más clara las áreas donde se realizan las entregas, facilitando la identificación de patrones geográficos importantes, como zonas con alta demanda. Esta información es útil para optimizar rutas de distribución, gestionar inventarios de forma localizada y mejorar la eficiencia operativa. Además, es posible notar que los grupos coinciden con divisiones lógicas de la ciudad (norte, sur, este y oeste), lo que respalda la validez de los clusters formados.
Este enfoque basado en K-means nos permite tomar decisiones basadas en datos y adaptadas a la geografía real de las entregas, en lugar de depender de límites administrativos que podrían no reflejar los verdaderos patrones de distribución.
Este gráfico permite la visualización de cuántas entregas se han realizado en cada cluster, mostrando la cantidad de entregas en un gráfico de barras. Las barras están ordenadas de mayor a menor, lo que facilita ver rápidamente en qué clusters hay más actividad
Este mapa permite la visualización de las áreas cubiertas por cada cluster en Buenos Aires, utilizando envolventes convexos para representar la extensión de cada grupo de entregas. En él, se muestran los puntos de datos de las entregas, coloreados según su cluster, lo que facilita identificar visualmente la distribución geográfica.
Este gráfico de barras muestra la cantidad de entregas realizadas por día de la semana, segmentadas por clusters geográficos. Cada barra representa el número de entregas realizadas en un día específico, y los diferentes colores identifican los clusters. Las barras están agrupadas en modo “dodge”, lo que facilita comparar las entregas entre clusters para cada día.
4.0.1.1Patrones Observados:
Los días con mayor cantidad de entregas son martes y miércoles, especialmente en el cluster 3 (color verde).
El cluster 2 (azul) tiene un comportamiento más uniforme a lo largo de la semana, sin días con picos destacados.
Los clusters 1 (rojo) y 4 (púrpura) muestran menor volumen de entregas en comparación, pero con actividad significativa los lunes y viernes.
Los días sábado también presentan un nivel de entregas importante, pero más distribuido entre los clusters.
Este gráfico ayuda a visualizar cómo se distribuyen las entregas durante la semana en las distintas zonas geográficas, permitiendo identificar picos de actividad por día y cluster, lo cual puede ser útil para planificar recursos logísticos y optimizar rutas de entrega.
Este gráfico de barras muestra la cantidad de entregas por cluster geográfico para los dos clientes, 20 y 70. Las barras están agrupadas para facilitar la comparación entre los clientes en cada cluster.
Se observa que el cliente 70 no tiene entregas registradas en el Cluster 1, mientras que el cliente 20 presenta entregas en todos los clusters. El Cluster 2 es donde se concentra el mayor número de entregas para ambos clientes, destacando un volumen particularmente alto para el cliente 20.
5 Análisis de series temporales
Tenemos 4 grandes caidas en las entregas por lo que buscamos identificar el motivo en cada fecha.
Por simplicidad asumimos que se debe a falta de datos en la fecha para comparir el dataset.
Para realizar un pronóstico de demanda para el mes de agosto vamos no vamos a tomar en cuenta estos 4 casos asumiendolos como irrelevantes para el modelo. Limitando los datos hasta el 30 de julio
Con los datos ordenados ahora podemos descomponer la serie temporal en tendencia, estacionalidad y residuales.
Con esta descomposición de la serie temporal, podemos analizar los siguientes aspectos clave:
Serie original (Data): Nos muestra los valores reales a lo largo del tiempo, permitiendo visualizar los cambios y fluctuaciones en los datos.
Componente estacional (Seasonal): Identifica los patrones repetitivos que ocurren en intervalos regulares, como ciclos mensuales o semanales. Este componente es útil para detectar estacionalidad, lo que puede ayudar a prever comportamientos futuros basados en estos ciclos.
Tendencia (Trend): Nos indica si los datos siguen una dirección general creciente, decreciente o estable a lo largo del tiempo. La tendencia es esencial para evaluar el comportamiento a largo plazo y detectar cambios estructurales.
Residuo (Remainder): Muestra las variaciones o anomalías que no se explican por la estacionalidad ni la tendencia. Este componente permite identificar eventos atípicos o ruido aleatorio que afecta los datos.
Esta descomposición permite entender los patrones subyacentes de la serie temporal, facilitando un análisis más preciso. Podemos, por ejemplo, diferenciar entre fluctuaciones temporales (estacionales) y cambios estructurales (tendencias), y evaluar la magnitud del ruido o error para mejorar modelos de predicción.
Series: train_ts
ARIMA(1,0,1) with non-zero mean
Coefficients:
ar1 ma1 mean
-0.5190 0.7919 380.8660
s.e. 0.2562 0.1729 9.5105
sigma^2 = 3140: log likelihood = -243.59
AIC=495.19 AICc=496.19 BIC=502.41
En los datos proporcionados no contamos con la información necesaria para realizar un análisis específico de las unidades de transporte, ya que sería indispensable disponer de un identificador único para cada vehículo. Sin embargo, incluimos esta sección como prueba de concepto para demostrar el valor que podría generar este tipo de análisis en la operación logística. Disponer de esta información permitiría evaluar aspectos fundamentales de la gestión de la flota, optimización de rutas y eficiencia operativa.
Enumeramos algunos análisis posibles con la información de los camiones, unidades de transporte.
Optimización de Rutas: Identificar las rutas más eficientes y reducir viajes innecesarios para minimizar costos y tiempo de entrega.
Utilización de la Flota: Evaluar el uso real de cada vehículo para detectar capacidad ociosa y ajustar la cantidad de unidades necesarias.
Desempeño Operativo: Medir tiempos activos (entregas) versus tiempos inactivos (espera o mantenimiento) para mejorar la eficiencia y planificación.
Algunos analisis que realizamos como prueba de concepto..
Primero identificamos la ubicacion de las sedes de Iflow y las comparamos con la primera entrega de cada día para entender si existe algún patron con respecto al orden de las entregas.
Luego graficamos la primera y segunda entrega de cada día. Esperaríamos que estos valores se encuentren en pares, muy cercanos entre si.
En este gráfico en el cual visualizamos los puntos de la primer y segunda entrega del día, encontramos cercanías entre los puntos azules (1er entrega del día) respecto a los puntos rojos (2da entrega del día) lo cual nos hace suponer que realizan las entregas de acuerdo a la cercanía con una ubicación específica.
Luego, tomando como referencia la fecha 2024-05-23, graficamos las primeras 20 entregas de ese día y las conectamos en orden cronológico.
Los camiones no llevan productos de distintos clientes, lleva o productos del cliente 20 o del 70.
Existen distintos camiones que podrian ser identificados gracias a las grandes distancias entre entregas con muy poco tiempo de diferencia.
En donde se realizan cruces de lineas podemos identificar rutas mal optimizadas.
6.1 Intentos de identificar transportistas.
A priori y para facilitar el análisis podríamos suponer que las entregas las realizan dos camiones. Uno para cada cliente. Podemos validar que esto es incorrecto al estudiar las entregas de un único cliente.
Vemos que entre la entrega 18 y 19 pasaron menos de 3min
Hay algunos casos donde podemos identificar camiones distintos:
Grandes distancias en poco tiempo.
Cruces de rutas.
Los cruces de trayectorias podrían indicar rutas ineficientes de los repartidores o que se estan siguiendo varios repartidores en la misma ruta. Podemos verlo de forma clara en el siguiente gráfico:
Habiendo identificado estas caracteristicas podríamos utilizar un modelo de agrupamiento para estimar distintos repartidores y sus trayectorias.
Esto tiene un gran valor para:
Identificar rutas ineficientes.
Estimar un tiempo de duración de cada entrega (Y rellenar vacios)
Identificar patrones en los errores de carga de datos (Duración de entregas)
Para la aplicación practica de estos analisis sería vital tener acceso real a los camiones o repartidores responsables de cada entrega pero para el bien de este analisis intentamos aproximar lo más posible a estos datos utilizando modelos estadisticos. Esperamos sirva como prueba de concepto para evaluar el potencial de recolectar estos datos.
Esto nos presenta un problema que podría solucionarse con coloración de grafos.
Construir el grafo de conflictos:
Nodos: Cada nodo representa una entrega individual con su respectiva latitud, longitud, fecha y hora.
Aristas (conflictos): Se dibuja una arista entre dos entregas si es imposible que hayan sido realizadas por el mismo repartidor debido a restricciones de tiempo y distancia.
Criterio de conflicto: Para dos entregas AAA y BBB, se calcula el tiempo mínimo necesario para que un repartidor viaje desde la ubicación de AAA a BBB considerando una velocidad razonable (por ejemplo, la velocidad promedio de un vehículo en esa área).
Si el tiempo transcurrido entre la hora de entrega de AAA y BBB es menor que el tiempo mínimo de viaje calculado, entonces se establece un conflicto entre AAA y BBB.
Algoritmos sugeridos:
Algoritmo Greedy de coloreo: Un enfoque sencillo que asigna colores a los nodos de forma secuencial, utilizando el menor número de colores posible en cada paso.
Heurísticas avanzadas: Si el grafo es grande y complejo, pueden emplearse algoritmos como DSATUR o técnicas metaheurísticas (algoritmos genéticos, recocido simulado) para aproximar una solución cercana al mínimo número de colores.
Algunos resultados obtenidos utilizando el algoritmo Greedy de coloreo con python:
Si bien funciona correctamente para alrededor de 20 entregas a medida que agregamos datos se dificulta el algoritmo y considerando las incosistencias de los datos y errores de carga no sería adecuado estimar usando este algoritmo en su estado actual.
Algunas consideraciones.
Toma en cuenta una velocidad promedio de los camiones arbitraira
Si bien podría ser interesante utilizar un algoritmo para resolver o estimar el problema. Evidentmente la mejor solución es tomar los datos de UNIGIS o un software automatico. De todas formas no se podrá extraer valor real con datos de tiempo erroneos.
Esta estimación nos presenta algunas preguntas sobre factores de las entregas.
Rutas optimizadas.
Gracias al análisis podemos ver que las rutas estan optimizadas pero parecen estar diseñadas para transportistas individuales y no para la flota como grupo. Optimizar las rutas de entregas para toda la flota como un ente es distinto a diseñar la ruta para cada camion entre dos puntos.
---title: "Caso de estudio | Iflow" date: "r Sys.Date()" # Inserta la fecha automáticamente format: htmltheme: darkly toc: true toc-location: left number-sections: true code-fold: true # Permitir plegado de bloques de código code-tools: true # Mostrar herramientas de copiado en los bloques de código smooth-scroll: true # Habilitar desplazamiento suave al navegar por el índice fig-align: center # Alinear las figuras al centro toc-depth: 3 # Profundidad máxima del índice lang: es # Establecer idioma del documento editor: visual # Editor visual habilitado---```{r,warning=FALSE,echo=FALSE, output=FALSE}# Limpieza de datoslibrary(tidyverse)library(dplyr)library(skimr)library(lubridate)# Analisis exploratoriolibrary(DataExplorer)library(inspectdf)library(plotly)library(viridis)# Análisis de series temporaleslibrary(tseries)library(forecast)# Clusteringlibrary(factoextra)# Claves de APIsmapbox_token <- "pk.eyJ1IjoibG9yZW5uem8iLCJhIjoiY20xcHYyd3g2MDk0bTJxb2k4YWZvOHlmcSJ9.r4E2pcTSM89NNHBFSmvKHw"# Cargamos los datosdata <- read_csv("data/raw_iflow_data.csv", show_col_types = FALSE)```# IntroducciónEn el marco del **Primer Desafío Internacional de la Red Latinoamericana de Ciencia de Datos**, este análisis tiene como propósito abordar un problema práctico del ámbito logístico. El proyecto promueve la colaboración entre estudiantes de diversas universidades latinoamericanas, fomentando el trabajo en equipo y la toma de decisiones basadas en datos reales.El **objeto de estudio** es un conjunto de datos proporcionado por **iFlow**, una empresa argentina especializada en logística integral, con operaciones tanto nacionales como internacionales dentro del MERCOSUR. iFlow se dedica a la gestión y co-gerencia de cadenas de abastecimiento para sus clientes, buscando optimizar procesos y mejorar la eficiencia operativa.Este análisis tiene como objetivo:1. **Comprender y describir** las principales características del conjunto de datos, que incluye registros de entregas realizadas en un período de tres meses.2. **Identificar patrones y tendencias** que permitan obtener insights relevantes sobre las operaciones de iFlow.3. **Detectar posibles inconsistencias o errores** en la base de datos, propias de un entorno operativo real, para evaluarlas e integrarlas al análisis.# Limpieza de datos.La primera etapa de este análisis consistió en la **limpieza de datos**. Este proceso de limpieza fue clave para preparar los datos para un análisis exploratorio robusto y la generación de insights confiables sobre la operación logística de iFlow.En primer lugar realizamos algunos cambios para **facilitar el trabajo** con los datos.- Tratar Columnas innecesarias La columna InicioVisitaPlanificado y FinVisitaPlanificado contienen los mismos valores por lo que las unificamos en una nueva columna.```{r,warning=FALSE,echo=FALSE}data <- data %>% # Nueva columna para almacenar el horario planificado mutate(visita_planificada = InicioVisitaPlanificado) %>% # Eliminamos InicioVisitaPlanificado y FinVisitaPlanificado dplyr::select(-InicioVisitaPlanificado, -FinVisitaPlanificado)```- Formatear correctamente las variables```{r,warning=FALSE,echo=FALSE}# Convertir columnas correspondientes a formato de fecha y horadata$InicioVisitaReal <- as.POSIXct(data$InicioVisitaReal, format="%Y-%m-%d %H:%M:%OS")data$FinVisitaReal <- as.POSIXct(data$FinVisitaReal, format="%Y-%m-%d %H:%M:%OS")data$visita_planificada <- as.POSIXct(data$visita_planificada, format="%Y-%m-%d %H:%M:%OS")# Las columnas InicioHorario1, FinHorario1, las pasamos a caracter para categorizarlas facilmente.data$InicioHorario1 <- as.character(data$InicioHorario1)data$FinHorario1 <- as.character(data$FinHorario1)# Pasamos variables categóricas a factores.data$cliente <- as.factor(data$cliente)```- Renombrar las columnas por nombres intuitivos.```{r ,warning=FALSE,echo=FALSE}# Renombrar columnas específicas con dplyrdata <- data %>% rename( id_orden = iddomicilioorden, inicio_horario = InicioHorario1, fin_horario = FinHorario1, bultos = Bultos, peso = Peso, unidades = Unidades, inicio_visita = InicioVisitaReal, fin_visita = FinVisitaReal)# Reorganizar columnas.data <- data %>% dplyr::select(id_orden, cliente, localidad, direccion, latitud, longitud, bultos, unidades, peso, inicio_horario, fin_horario, visita_planificada, inicio_visita, fin_visita)```Con estos cambios realizados pasamos a modificaciones y **arreglos necesarios** para un análisis correcto de los datos.- Eliminación de filas duplicadas.```{r,warning=FALSE,echo=FALSE}data <- data %>% distinct()```- Arreglo de coordenadas faltantes en algunas entregas. El dato de coordenadas en algunas filas estaba vacio o indicaba "0". En algunos de estos casos pudimos rellenar estas coordenadas con datos existentes del domicilio (21 filas). En caso de que esto no sea posible las filas fueron eliminadas (19 filas) y no serán tomadas en cuenta para el análisis.```{r,warning=FALSE,echo=FALSE}# Filtrar las filas donde latitud o longitud son NAcordenadas_vacias <- data %>% filter( is.na(latitud) | is.na(longitud) | latitud == 0 | longitud == 0 )# cordenadas_vacias # dim 43 x 14# Filtrar las observaciones donde id_orden está en cordenadas_vaciasobservaciones_id_orden <- data %>% filter(id_orden %in% cordenadas_vacias$id_orden) %>% group_by(id_orden) %>% summarise(count = n())# Mostrar el resultado# observaciones_id_orden# Contar las apariciones de cada id_orden en cordenadas_vaciasapariciones_cordenadas_vacias <- cordenadas_vacias %>% group_by(id_orden) %>% summarise(na_count = n())# Unir las tablas por id_ordenresultado <- observaciones_id_orden %>% left_join(apariciones_cordenadas_vacias, by = "id_orden") %>% # Si no hay coincidencias en cordenadas_vacias, establecer na_count en 0 mutate(na_count = ifelse(is.na(na_count), 0, na_count)) %>% # Restar las apariciones de cordenadas_vacias del total mutate(count_diff = count - na_count) %>%# Filtrar solo los id_orden donde count_diff es mayor a 0 filter(count_diff > 0)# Mostrar el resultado#resultado``````{r,warning=FALSE,echo=FALSE}# Definir la función que revisa y sobrescribe latitud y longitudreparar_lat_long <- function(dataset, ids) { # Iterar sobre cada id de la lista for (id in ids) { # Filtrar las observaciones válidas de latitud y longitud para este id_orden observaciones_validas <- dataset %>% filter(id_orden == id & !is.na(latitud) & !is.na(longitud) & latitud != 0 & longitud != 0) # Si existen observaciones válidas, tomar la primera ocurrencia if (nrow(observaciones_validas) > 0) { latitud_valida <- observaciones_validas$latitud[1] longitud_valida <- observaciones_validas$longitud[1] # Sobrescribir las observaciones con latitud o longitud nulos o 0 dataset <- dataset %>% mutate( latitud = ifelse(id_orden == id & (is.na(latitud) | latitud == 0), latitud_valida, latitud), longitud = ifelse(id_orden == id & (is.na(longitud) | longitud == 0), longitud_valida, longitud) ) } } # Retornar el dataset reparado return(dataset)}``````{r,warning=FALSE,echo=FALSE}# Ejecutar la función usando los id_orden de la columna resultadoids_a_reparar <- resultado$id_orden# Aplicar la función a datadata <- reparar_lat_long(data, ids_a_reparar)```Por último creamos algunas nuevas columnas para distintos análisis. Entre estas algunas columnas para facilitar la interacción con fechas y horarios de entregas.```{r,warning=FALSE,echo=FALSE, output=FALSE}# Asegurar que los días se generen en españolSys.setlocale("LC_TIME", "es_ES.UTF-8") # Crear la columna 'dia_str' con normalización de caracteresdata <- data %>% mutate( dia = as.integer(format(fin_visita, "%d")), mes = as.integer(format(fin_visita, "%m")), hora = as.integer(format(fin_visita, "%H")), diferencia_minutos = as.numeric( difftime(fin_visita, visita_planificada, units = "mins")), dia_str = tolower(iconv(weekdays(fin_visita, abbreviate = FALSE), to = "UTF-8")), duracion_visita_min = as.numeric( difftime(fin_visita, inicio_visita, units = "mins")), duracion_visita_horas = as.numeric( difftime(fin_visita, inicio_visita, units = "hours")) )# Guardamos los datos limpios# write.csv(x = data, file = "iflow_clean.csv")``````{r,warning=FALSE,echo=FALSE}data <- read.csv("data/clean_iflow_data.csv")```# Vista general.En esta sección ofrecemos una **visión superficial de los datos**, brindando un panorama inicial que permite familiarizarnos con su estructura y contenido.```{r,warning=FALSE,echo=FALSE, output=FALSE}# Cuantas entregas tenemos en total?dim(data)dim(data %>% filter(cliente==20))dim(data %>% filter(cliente==70))```Se registraron 27.419 entregas de dos clientes distintos: 16,545 del cliente 20 y 10.874 del cliente 70.Todas estas entregas fueron realizadas entre el 3 de mayo y 8 de agosto de 2024, con un promedio de 9.005 entregas por mes```{r,warning=FALSE,echo=FALSE, output=FALSE}# Ensure fin_visita is in the correct POSIXct formatdata$fin_visita <- as.POSIXct(data$fin_visita, format = "%Y-%m-%d %H:%M:%OS")# Filter the row with the maximum fin_visitadata %>% filter(fin_visita == min(fin_visita, na.rm = TRUE))data %>% filter(fin_visita == max(fin_visita, na.rm = TRUE))```::: panel-tabset## Entregas por cliente```{r,warning=FALSE,echo=FALSE}# Crear una columna con el primer día del mes correspondientedata <- data %>% mutate(mes = as.Date(floor_date(fin_visita, "month"))) # Asegurar que 'mes' sea Date# Agrupar por mes y contar la cantidad de entregasentregas_por_mes <- data %>% group_by(mes, cliente) %>% summarise(n = n(), .groups = "drop")entregas_por_mes$cliente <- as.factor(entregas_por_mes$cliente)# Crear el gráfico de barrasggplot(entregas_por_mes, aes(x = mes, y = n, fill=cliente)) + geom_bar(stat = "identity", position="dodge") + scale_x_date(date_labels = "%b %Y", date_breaks = "1 month") + labs(title = "Cantidad de Entregas por Mes por cliente", x = "Mes", y = "Número de Entregas") + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1))```Este gráfico de barras muestra la cantidad de entregas realizadas cada mes, separadas por cliente. Las barras están agrupadas por mes y se distinguen por colores para cada cliente.La cantidad de entregas es constante entre mayo y julio, con algunas variaciones entre los clientes. Esto indica un comportamiento predecible en la demanda mensual. Planificar recursos en función de estos patrones puede mejorar la eficiencia operativa.## Entregas por mes```{r,warning=FALSE,echo=FALSE}# Crear una columna con el primer día del mes correspondientedata <- data %>% mutate(mes = as.Date(floor_date(fin_visita, "month"))) # Asegurar que 'mes' sea Date# Agrupar por mes y contar la cantidad de entregasentregas_por_mes <- data %>% group_by(mes) %>% summarise(n = n(), .groups = "drop")# Crear el gráfico de barrasggplot(entregas_por_mes, aes(x = mes, y = n)) + geom_bar(stat = "identity", fill = "#94C11F") + scale_x_date(date_labels = "%b %Y", date_breaks = "1 month") + labs(title = "Cantidad de Entregas por Mes", x = "Mes", y = "Número de Entregas") + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1))```En este gráfico se visualiza la cantidad total de entregas realizadas por mes, sin importar el cliente.n patrón estable en el número de entregas permite predecir la demanda mensual. Las caídas pueden ser analizadas más adelante para identificar feriados o problemas en la operación.:::## ¿Cuales son los horarios de entrega? {#horarios-entrega}```{r, warning=FALSE, echo=FALSE}# Agrupar los datos por día y hora para contar ocurrenciasresumen <- data %>% group_by(dia_str, hora) %>% summarise(n = n(), .groups = "drop") %>% ungroup()# Asegurar el orden correcto de los días (Lunes a Domingo)resumen$dia_str <- factor(resumen$dia_str, levels = c("lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"))resumen <- resumen %>% filter(!is.na(dia_str), dia_str != "domingo")ggplot(resumen, aes(x = hora, y = dia_str, fill = n)) + geom_tile(color = "white") + scale_fill_viridis(option = "C", direction = 1) + labs(title = "Entregas por día y hora", x = "Hora del día", y = "Día de la semana", fill = "Cantidad") + theme_minimal()```Este gráfico de calor muestra la cantidad de entregas realizadas según la hora del día y el día de la semana. Cada celda del gráfico representa un cruce entre una hora y un día, con colores más intensos indicando una mayor cantidad de entregas.Mayor actividad: El miércoles alrededor de las 15:00 es el momento con más entregas. Patrón semanal: Hay menos actividad los fines de semana, especialmente los domingos, donde casi no se registran entregas.```{r,warning=FALSE,echo=FALSE}# Convertir la columna dia_str en un factor ordenadodata <- data %>% mutate(dia_str = factor(dia_str, levels = c("lunes", "martes", "miércoles", "jueves", "viernes", "sábado","domingo")))# Agrupar por el nombre del día y contar las entregasentregas_por_dia <- data %>% group_by(dia_str) %>% summarise(n = n())# Crear el gráfico de barrasggplot(entregas_por_dia, aes(x = dia_str, y = n)) + geom_bar(stat = "identity", fill = "#94C11F") + labs(title = "Cantidad de Entregas por Día de la Semana", x = "Día de la Semana", y = "Número de Entregas") + theme_minimal()```Vemos que el domingo hay solo 3 entregas. Esto podría deberse a un error por lo que las revisamos.```{r,warning=FALSE,echo=FALSE, output=FALSE}data %>% filter(dia_str == "domingo")```| Orden | Localidad | Fecha y hora de entrega ||--------|-----------------|-------------------------|| 81943 | CAPITAL FEDERAL | 2024-07-21 23:51:00 || 100968 | CAPITAL FEDERAL | 2024-07-21 23:51:00 || 100968 | CAPITAL FEDERAL | 2024-07-21 23:51:00 |Al revisar estas entregas encontramos que las tres fueron entregadas a las 23:51. Esto podría deberse a un error en la carga de datos o a entregas especiales. Al ser las últimas entregas del día puede deberse a un cierre automático del sistema despues de haber dejado entregas inciadas el día anterior sin marcar su finalización.Si bien este parece ser un caso puntual existen dos grandes problemas respecto a los horarios y duración de las entregas. Estos problemas son.1. **Duración de las entregas:** Muchas entregas tienen el mismo horario de inicio y fin. Esto puede deberse a errores en la carga de datos o a un sistema de carga poco eficiente.2. **Entregas consecutivas:** Muchas entregas consecutivas fueron cargadas con el horario de la entrega anterior. Esto dificulta la identificación de rutas, estimación de tiempos muertos y duraciones reales entre entregas.## Errores de carga en horarios de entrega.Notamos que en las entregas muy cercanas geograficamente, en la misma cuadra, suelen tener el mismo horario de finalización. Esto se puede deber a que los operarios olvidan hacer la carga individual o consideran mas rapido completar ambas entregas antes de registrarlo en el sistema.Junto con los errores de carga en los horarios de entrega este puede ser un segundo indicador de que el sistema de carga puede ser mejorado para no recolectar datos erroneos en el futuro.Horarios cargados de forma incorrecta podrian causar:1. Mala estimación sobre tiempos muertos.2. Dificulta optimizar los procesos de entrega.3. Perjudica la proyección de horarios de entregas o ventanas horarias.Algunas sugerencias e ideas para mejorar esto incluyen:- Mejoras de la interfaz en el sistema de carga para facilitar y fomentar su uso.- Implementación de un sistema de validación de los datos para evitar duplicados.- Desarrollo y uso de hardware específico para la carga de datos.> ¿Cuál es el tiempo promedio entre el inicio y fin de las visitas de entrega?```{r, warning=FALSE, echo=FALSE}# dim(data %>% filter(inicio_visita != fin_visita)) # 17142# data %>% filter(inicio_visita == fin_visita) # 10255# Calcular los totales y proporcionestotal <- nrow(data) # Total de filasdif_visit <- nrow(data %>% filter(inicio_visita != fin_visita)) # 17142 filas diferentesigual_visit <- nrow(data %>% filter(inicio_visita == fin_visita)) # Filas iguales# Crear un dataframe con los resultadosresumen <- data.frame( Categoria = c("Inicio ≠ Fin", "Inicio = Fin"), Conteo = c(dif_visit, igual_visit))# Calcular el porcentaje para cada categoríaresumen$Porcentaje <- round((resumen$Conteo / total) * 100, 2)# Crear el gráfico de barrasggplot(resumen, aes(x = Categoria, y = Conteo, fill = Categoria)) + geom_bar(stat = "identity", width = 0.6) + geom_text(aes(label = paste0(Porcentaje, "%")), vjust = -0.5, size = 5) + # Etiquetas con porcentaje arriba de las barras labs(title = "Comparación de visitas", x = "Categoría", y = "Cantidad de visitas") + theme_minimal()```Bien cargados 17142 vs mal cargados 10255 mal. Pero encontramos que son incluso más. Una forma de solucionarlo podría haber sido ordenar las entregas en orden cronologico e intentar estimar la duración real segun el tiempo entre las entregas y la distancia entre ellas pero al intentar esto encontramos que muchas entregas consecutivas arrastran errores. Dificultando la identificación de rutas, estimación de tiempos muertos y duraciones reales entre entregas.```{r, warning=FALSE, echo=FALSE, output=FALSE}# Ordenar los datos cronológicamente (asumo que tienes una columna de timestamp)data_ordenada <- data %>% arrange(inicio_visita)# Crear una columna que identifique si el fin_visita es igual al de la fila anteriordata_secuencia <- data_ordenada %>% mutate( consecutivo = (fin_visita == lag(fin_visita, default = first(fin_visita))) )# Crear un identificador para cada grupo consecutivo con el mismo fin_visitadata_secuencia <- data_secuencia %>% mutate( grupo = cumsum(!consecutivo) # Incrementar grupo cuando cambia fin_visita )# Resumir el número de filas en cada grupo consecutivoresumen_secuencias <- data_secuencia %>% group_by(fin_visita, grupo) %>% summarise(cantidad = n(), .groups = "drop")# Mostrar las secuencias más largasresumen_secuencias %>% arrange(desc(cantidad))filtered_amounts <- resumen_secuencias %>% filter(cantidad != 1)```Como referencia, el día 2024-06-27 16:06:00 hay 23 entregas graficadas el mismo día.::: panel-tabset## Ejemplo 50 entregas```{r, warning=FALSE, echo=FALSE} #Cargar librerías necesariaslibrary(leaflet)library(dplyr)# Filtrar las entregas según el fin_visita elegidofin_visita_elegido <- "2024-06-12 16:12:00" # Cambia este valor por el deseadoentregas_filtradas <- data %>% filter(fin_visita == fin_visita_elegido)# Verificar si hay datos para graficarif (nrow(entregas_filtradas) == 0) { print("No hay entregas con el fin_visita seleccionado.")} else { # Crear el mapa interactivo con Leaflet leaflet(data = entregas_filtradas) %>% addTiles() %>% # Añadir un mapa base (OpenStreetMap) addCircleMarkers( lng = ~longitud, lat = ~latitud, # Coordenadas radius = 6, color = "blue", stroke = FALSE, fillOpacity = 0.8, fillColor = "red", # Estilo de los marcadores label = ~paste("Lat:", latitud, "<br>Lng:", longitud), # Etiquetas al pasar el mouse popup = ~paste0("Entrega en: ", latitud, ", ", longitud) # Popup al hacer clic ) }```## Ejemplo 23 entregas```{r, warning=FALSE, echo=FALSE} #Cargar librerías necesariaslibrary(leaflet)library(dplyr)# Filtrar las entregas según el fin_visita elegidofin_visita_elegido <- "2024-06-27 16:06:00" # Cambia este valor por el deseadoentregas_filtradas <- data %>% filter(fin_visita == fin_visita_elegido)# Verificar si hay datos para graficarif (nrow(entregas_filtradas) == 0) { print("No hay entregas con el fin_visita seleccionado.")} else { # Crear el mapa interactivo con Leaflet leaflet(data = entregas_filtradas) %>% addTiles() %>% # Añadir un mapa base (OpenStreetMap) addCircleMarkers( lng = ~longitud, lat = ~latitud, # Coordenadas radius = 6, color = "blue", stroke = FALSE, fillOpacity = 0.8, fillColor = "red", # Estilo de los marcadores label = ~paste("Lat:", latitud, "<br>Lng:", longitud), # Etiquetas al pasar el mouse popup = ~paste0("Entrega en: ", latitud, ", ", longitud) # Popup al hacer clic ) }```:::¿Que tan frecuente es este error? Muy frecuentes```{r, warning=FALSE, echo=FALSE}# Asegurarse de que no haya NAs en la columna 'cantidad'resumen_secuencias <- resumen_secuencias %>% filter(!is.na(cantidad), cantidad != 1)# Crear la columna 'categoria' con las condiciones bien definidasresumen_secuencias <- resumen_secuencias %>% mutate( categoria = case_when( cantidad == 2 ~ "2", cantidad >= 3 & cantidad <= 5 ~ "De 3 a 5", cantidad >= 6 & cantidad <= 10 ~ "De 6 a 10", cantidad >= 11 & cantidad <= 20 ~ "De 11 a 20", cantidad > 20 ~ "Más de 20" ) )# Verificar si hay NAs en la columna 'categoria'resumen_secuencias <- resumen_secuencias %>% filter(!is.na(categoria))# Crear el gráfico de barras con las categorías corregidasggplot(resumen_secuencias, aes(x = categoria)) + geom_bar(fill = "#94C11F", color = "black", alpha = 0.8) + labs( title = "Distribución de Secuencias por Categoría", x = "Categoría de cantidad", y = "Frecuencia" ) + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotar etiquetas del eje X```Se registraron **12,799 entregas con horarios incorrectos**, lo que representa el **46.67%** de los datos cargados de forma errónea.Con casi la mitad de los datos poco confiables, cualquier análisis sobre la **eficiencia operativa** pierde precisión. Esto puede generar errores importantes al evaluar la **duración de las entregas**, los **periodos de actividad** y las **rutas utilizadas**.Corregir la recopilación de estos datos es una **oportunidad clave** para mejorar el crecimiento de la empresa, optimizar las **ventanas de entrega**, identificar **cuellos de botella** reales y facilitar una toma de decisiones más precisa y efectiva.> "Lo que no se mide, no se puede mejorar." - Peter F. DruckerA continuación, se presentan posibles causas y oportunidades para resolver esta situación.1. Manual de uso y capacitación sobre el sistema [UNIGIS](https://www.unigis.com/ "Sitio web oficial de Unigis")Basandonos en el [Manual de transportistas - Elementos de seguridad y APPS](https://transportes.iflow21.com/portal/es/kb/articles/manual-de-transportistas-elementos-de-seguridad-y-apps) encontramos la siguiente referencia sobre el uso de la aplicación.Capacitar mejor al personal con mayor cantidad de recursos, claridad en los instructivos y videos demostrando el uso correcto del sistema [videos demostrando el uso correcto del sistema](https://www.youtube.com/watch?v=JnEHVHhs6V4 "Video grabado por Corporación Aceros Arequipa para su equipo") mejoraría la precision de la carga de los datos en el futuro.2. Creación de una interfaz personalizada para Unigis.El error de carga de horarios iguales en entregas consecutivas puede deberse principalmente a la dificultad de uso del sistema o poca practicidad del mismo por la que los transportistas podrían saltear los pasos del instructivo.Si migrar a un nuevo sistema más moderno es una alternativa muy costosa podrían considerar hacer una inversión en desarrollo frontend para, utilizando la API del sistema actual, puedan tener una interfaz más amena a los transportistas.[](https://www.postman.com/irampoldi/unigis/request/8833s18/reportedeviajes)El desarrollo de una interfaz personalizada para interactuar con su sistema actual podría ser una inversión considerable pero economica contrastando con la posibilidad de un desarrollo personalizado o la migración a un nuevo sistema.Algunas consideraciones:- Inversión en equipo e investigación UX para asegurar el uso intuitivo de los transportistas. Es importante entender como es el uso del sistema en la practica.3. Migrar a un sistema más moderno o diseñar uno a medida para sus necesidades. Puede ser la opción mas costosa.Como conclusión mejorar la capacitación del personal, proporcionando más recursos, instructivos claros y videos demostrativos sobre el uso correcto del sistema, ayudaría a aumentar la precisión en la carga de datos. Además, la creación de una interfaz personalizada para Unigis podría resolver errores comunes, como los horarios idénticos en entregas consecutivas, que probablemente ocurren por la complejidad o poca practicidad del sistema, lo que lleva a los transportistas a omitir pasos. Si migrar a un sistema más moderno resulta demasiado costoso, una opción sería invertir en el desarrollo de un frontend más amigable, utilizando la API del sistema actual, para facilitar su uso y mejorar la experiencia de los transportistas.## Información de bultos y peso.En promedio, cada entrega contiene:- **28.40 unidades**- **5.75 bultos**- **41.15 kg de peso**El **peso elevado por pedido** sugiere la presencia de productos voluminosos o de alto consumo, lo que puede aumentar los **costos de transporte** debido a la mayor carga. Esto también tiene implicaciones importantes para la **planificación de rutas**, ya que es necesario optimizar el uso de los vehículos para evitar sobrecargas y cumplir con los límites de capacidad. Además, la gestión eficiente de estos pedidos requiere ajustar los tiempos de entrega, garantizar la disponibilidad de unidades adecuadas y considerar posibles restricciones en ciertas rutas debido al peso total transportado.::: panel-tabset## 🔢 Unidades::: panel-tabsetEste histograma permite ver la concentración de pedidos en ciertos rangos de unidades. Si se observa una mayor densidad en valores específicos, esto podría indicar patrones de demanda estándar entre los clientes.:::```{r,warning=FALSE,echo=FALSE}# Crear el histogramaggplot(data, aes(x = unidades)) + geom_histogram(binwidth = 50, fill = "#94C11F", color = "black") + labs(title = "Histograma de Unidades", x = "Bultos", y = "Frecuencia") + theme_minimal()``````{r,warning=FALSE,echo=FALSE}mean(data$unidades) # Promedio```## 📦 Bultos::: panel-tabsetEste histograma permite identificar el tamaño típico de los pedidos. Una distribución sesgada hacia valores altos sugiere que la mayoría de los pedidos son de varios bultos, lo que puede influir en la planificación de cargas y en la selección de vehículos de transporte.:::```{r,warning=FALSE,echo=FALSE}# Crear el histogramaggplot(data, aes(x = bultos)) + geom_histogram(binwidth = 5, fill = "#94C11F", color = "black") + labs(title = "Histograma de Bultos", x = "Unidades", y = "Frecuencia") + theme_minimal()``````{r,warning=FALSE,echo=FALSE}mean(data$bultos) # Promedio```## ⚖️ Peso::: panel-tabsetEste histograma permite identificar el tamaño típico de los pedidos. Una distribución sesgada hacia valores altos sugiere que la mayoría de los pedidos son de varios bultos, lo que puede influir en la planificación de cargas y en la selección de vehículos de transporte.:::```{r,warning=FALSE,echo=FALSE}# Crear el histogramaggplot(data, aes(x = peso)) + geom_histogram(binwidth = 50, fill = "#94C11F", color = "black") + labs(title = "Histograma de Peso", x = "Unidades", y = "Frecuencia") + theme_minimal()``````{r,warning=FALSE,echo=FALSE}mean(data$peso) # Promedio```:::## Distribución geográfica de las entregas.```{r,echo=FALSE, output=FALSE}# Geolocalizaciónlibrary(sf)library(raster)```::: panel-tabset## Todas las entregas```{r,warning=FALSE,echo=FALSE}data$cliente <- as.factor(data$cliente)# Graficar las entregas por cliente con colores distintosplot_ly( data, lat = ~latitud, lon = ~longitud, type = 'scattermapbox', mode = 'markers', color = ~cliente, # Asigna un color distinto por cliente marker = list(size = 7, opacity = 0.3), # Ajusta el tamaño y la transparencia de los marcadores text = ~paste("Cliente:", cliente, "<br>Dirección:", direccion) # Información al pasar el mouse) %>% layout( mapbox = list( accesstoken = mapbox_token, center = list(lat = -34.6037, lon = -58.3816), # Coordenadas de Buenos Aires zoom = 10, # Nivel de zoom style = "open-street-map" # Estilo del mapa ), title = "Mapa de Entregas en Buenos Aires por Cliente", margin = list(r = 0, t = 0, b = 0, l = 0) )```## Entregas por dia de la semana```{r,warning=FALSE,echo=FALSE}# Crear un factor ordenado para los días de la semanadata$dia_str <- factor(data$dia_str, levels = c("lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"))# Crear una lista de días únicos (ahora ya estarán ordenados según el factor)dias_unicos <- unique(data$dia_str)# Crear una lista de "frames" para cada díaframes <- lapply(dias_unicos, function(dia) { list( name = dia, data = list( list( type = "scattermapbox", lat = data$latitud[data$dia_str == dia], lon = data$longitud[data$dia_str == dia], mode = "markers", marker = list(size = 7, opacity = 0.5), text = paste("Cliente:", data$cliente[data$dia_str == dia], "<br>Dirección:", data$direccion[data$dia_str == dia]), color = data$cliente[data$dia_str == dia] ) ) )})# Graficar las entregas por cliente con un slider para cambiar día a díaplot_ly( data, lat = ~latitud, lon = ~longitud, type = 'scattermapbox', mode = 'markers', color = ~cliente, # Asigna un color distinto por cliente frame = ~dia_str, # Agregar el día como frame para la animación (ahora en orden) marker = list(size = 7, opacity = 0.6), # Ajusta el tamaño y la transparencia de los marcadores text = ~paste("Cliente:", cliente, "<br>Dirección:", direccion) # Información al pasar el mouse) %>% layout( mapbox = list( accesstoken = mapbox_token, center = list(lat = -34.6037, lon = -58.3816), # Coordenadas de Buenos Aires zoom = 10, # Nivel de zoom style = "open-street-map" # Estilo del mapa ), title = "Mapa de Entregas en Buenos Aires por Cliente", margin = list(r = 5, t = 25, b = 5, l = 5) ) %>% animation_opts( frame = 500, # Duración de cada frame en milisegundos transition = 0, # Sin transiciones entre frames redraw = TRUE ) %>% animation_slider( currentvalue = list(prefix = "Día: ") )```:::Como podemos ver este mapa de entregas de la ciudad de Buenos Aires muestra visualmente la dispersión de las entregas en la ciudad. La variedad de colores permite ver qué clientes se agrupan en ciertas áreas. Si ciertos clientes concentran entregas en una región específica, eso puede indicar una demanda localizada o preferencia regional de ciertos productos.```{r,warning=FALSE,echo=FALSE}# Crear una lista de fechas únicas extraídas de la columna fin_visitafechas_unicas <- unique(as.Date(data$fin_visita))# Graficar las entregas por cliente con un slider para cambiar por fechaplot_ly( data, lat = ~latitud, lon = ~longitud, type = 'scattermapbox', mode = 'markers', color = ~cliente, # Asigna un color distinto por cliente frame = ~as.Date(fin_visita), # Agregar la fecha como frame para la animación marker = list(size = 7, opacity = 0.7), # Ajusta el tamaño y la transparencia de los marcadores text = ~paste("Cliente:", cliente, "<br>Dirección:", direccion) # Información al pasar el mouse) %>% layout( mapbox = list( accesstoken = mapbox_token, center = list(lat = -34.6037, lon = -58.3816), # Coordenadas de Buenos Aires zoom = 10, # Nivel de zoom style = "open-street-map" # Estilo del mapa ), title = "Mapa de Entregas en Buenos Aires por Cliente", margin = list(r = 0, t = 0, b = 0, l = 0) ) %>% animation_opts( frame = 500, # Duración de cada frame en milisegundos transition = 0, # Sin transiciones entre frames redraw = TRUE ) %>% animation_slider( currentvalue = list(prefix = "Fecha: ") )```En este mapa las áreas con mayor densidad de entregas resaltan visualmente. Estas zonas de alta actividad pueden representar zonas comerciales o residenciales clave. Esto es útil para optimizar las rutas de entrega y asignar más recursos a zonas con alta demanda.Para verlo de forma más resumida;::: panel-tabset## 🏘️ BarriosEn base a este mapa se permite ver cómo se distribuyen las entregas a nivel de barrio. Los barrios con mayor intensidad de color son aquellos con más entregas, lo que indica la importancia de estos barrios en el volumen de pedidos.```{r,warning=FALSE,echo=FALSE}# Cargar los barrios desde el archivo GeoJSONbarrios.comp <- st_read("maps/barrios.geojson", quiet = TRUE) # Reemplaza con la ruta correctabarrios <- barrios.comp[, c("BARRIO", "geometry")]# Ver los nombres de las columnas del GeoDataFrame de barrios# Filtrar las entregas con coordenadas válidas y crear un objeto sfdata_sf <- data %>% filter(!is.na(latitud) & !is.na(longitud)) %>% st_as_sf(coords = c("longitud", "latitud"), crs = 4326) # Sistema de coordenadas WGS 84# Unir cada entrega con su barrio correspondienteentregas_por_barrio <- st_join(data_sf, barrios)# Agrupar por el campo "BARRIO" y contar el total de entregasentregas_agrupadas <- entregas_por_barrio %>% group_by(BARRIO) %>% summarise(total_entregas = n())# Unir la información agregada de entregas al GeoDataFrame de barriosbarrios <- barrios %>% st_join(entregas_agrupadas)# Rellenar valores NA (barrios sin entregas) con 0barrios$total_entregas[is.na(barrios$total_entregas)] <- 0# Crear el mapa con ggplot2ggplot(data = barrios) + geom_sf(aes(fill = total_entregas)) + # Colorear según la cantidad de entregas scale_fill_viridis_c(option = "plasma", na.value = "white") + # Paleta de colores theme_minimal() + labs( title = "Cantidad de Entregas por Barrio en Buenos Aires", fill = "Entregas" )```## 🏙️ ComunasSimilar al mapa por barrio, pero agrupando entregas por comunas. Las comunas que presentan más entregas destacan como puntos de interés para analizar el impacto logístico y la demanda concentrada.```{r, warning=FALSE, echo=FALSE}# 1. Cargar los barrios desde el archivo GeoJSONbarrios.comp <- st_read("maps/barrios.geojson", quiet = TRUE) # Ajusta la ruta según corresponda# 2. Agrupar los polígonos por "COMUNA"comunas <- barrios.comp %>% group_by(COMUNA) %>% summarise(geometry = st_union(geometry)) # Unir los polígonos por comuna# Asegurarse de que COMUNA sea textocomunas$COMUNA <- as.character(comunas$COMUNA)# 3. Filtrar las entregas con coordenadas válidas y convertirlas a un objeto sfdata_sf <- data %>% filter(!is.na(latitud) & !is.na(longitud)) %>% st_as_sf(coords = c("longitud", "latitud"), crs = 4326)# 4. Asignar cada entrega a su comuna correspondiente usando st_joinentregas_por_comuna <- st_join(data_sf, comunas)# 5. Agrupar por "COMUNA" y contar el total de entregasentregas_agrupadas <- entregas_por_comuna %>% group_by(COMUNA) %>% summarise(total_entregas = n())# 6. Unir los datos de entregas agregados al GeoDataFrame de comunascomunas <- comunas %>% st_join(entregas_agrupadas)# 7. Rellenar los valores NA (comunas sin entregas) con 0comunas$total_entregas[is.na(comunas$total_entregas)] <- 0# 8. Crear el mapa con ggplot2ggplot(data = comunas) + geom_sf(aes(fill = total_entregas)) + scale_fill_viridis_c(option = "plasma", na.value = "white") + theme_minimal() + labs( title = "Cantidad de Entregas por Comuna en Buenos Aires", fill = "Entregas" )```## 🌡️ Entregas IndividualesLos puntos en el mapa de Buenos Aires representan ubicaciones de entregas, y las áreas con mayor densidad de entregas aparecen con colores más intensos, permitiendo identificar visualmente las zonas con más actividad de entregas.```{r, warning=FALSE, echo=FALSE}# Mapa de calor de entregasheatmap_data <- data %>% group_by(latitud, longitud) %>% summarise(total_entregas = n())# Graficar un mapa de calor para visualizar las zonas con mayor densidad de entregasheatmap_plot <- plot_ly( heatmap_data, lat = ~latitud, lon = ~longitud, z = ~total_entregas, type = 'densitymapbox', colorscale = 'Viridis', radius = 10) %>% layout( mapbox = list( accesstoken = mapbox_token, center = list(lat = -34.6037, lon = -58.3816), zoom = 10, style = "open-street-map" ), title = "Mapa de Calor de Entregas en Buenos Aires", showlegend = FALSE, # Ocultar la leyenda margin = list(r = 0, t = 60, b = 0, l = 0) # Agregar más espacio en la parte superior )# Mostrar el mapa de calorheatmap_plot```:::### Centro de distribución en Mendoza.::: panel-tabset## 📍 Mapa```{r,warning=FALSE,echo=FALSE}# Crear un gráfico usando Plotly y Mapboxfig <- plot_ly( data = data, type = 'scattermapbox', mode = 'markers', lat = ~latitud, lon = ~longitud, marker = list(size = 8, color = 'blue', opacity = 0.7))# Configurar el estilo de Mapbox (puedes cambiar el estilo)fig <- fig %>% layout( mapbox = list( style = 'carto-positron', # Otros estilos: 'open-street-map', 'stamen-terrain', etc. zoom = 2, # Nivel de zoom center = list(lat = mean(data$latitud), lon = mean(data$longitud)) # Centrado en los datos ), margin = list(t = 0, b = 0, l = 0, r = 0) # Margen para ajustar el espacio del gráfico )# Mostrar el gráficofig```## 🖼️ Screenshot:::> ¿Cuántas entregas se hicieron fuera del tiempo esperado o planificado?```{r, warning=FALSE, echo=FALSE}# Crear una nueva columna que clasifique si llegó tarde o tempranodata <- data %>% mutate(estado_entrega = ifelse(diferencia_minutos > 0, "Tarde", "Temprano o a Tiempo"))# Agrupar los datos por mes y estado de entregadata_agrupada <- data %>% group_by(mes, estado_entrega) %>% summarise(cantidad = n())# Graficar con ggplotggplot(data_agrupada, aes(x = mes, y = cantidad, fill = estado_entrega)) + geom_bar(stat = "identity", position = "dodge") + labs( title = "Cantidad de Entregas por Mes", x = "Mes", y = "Cantidad de Entregas", fill = "Estado de Entrega" ) + theme_minimal()```Este gráfico permite la visualización de la cantidad de entregas por mes, clasificadas en "Tarde" y "Temprano o a Tiempo".> ¿Qué clientes generan más volumen de entregas y cuáles presentan más problemas o irregularidades?```{r, warning=FALSE, echo=FALSE}# Crear el mapa con plotly y mapboxfig <- plot_ly( data = data, lat = ~latitud, lon = ~longitud, color = ~factor(cliente), # Colorear según cliente colors = c("dodgerblue", "tomato"), # Colores personalizados para cada cliente type = 'scattermapbox', mode = 'markers', marker = list( size = 10, # Tamaño del marcador opacity = 0.2 # Ajustar transparencia (alpha) ), text = ~paste( "Latitud: ", latitud, "<br>", "Longitud: ", longitud, "<br>", "Cliente: ", cliente, "<br>", "ID Orden: ", id_orden, "<br>", "Fecha: ", fin_visita ), # Información a mostrar en hover hoverinfo = 'text' # Mostrar solo el texto personalizado)# Configuración del mapa centrado en Buenos Airesfig <- fig %>% layout( mapbox = list( style = 'open-street-map', # Estilo del mapa zoom = 10, # Ajuste del nivel de zoom para Buenos Aires center = list(lat = -34.6037, lon = -58.3816) # Centrar en Buenos Aires ), title = "Mapa de Entregas por Cliente" )# Mostrar el mapafig```Este mapa permite la visualización de un mapa interactivo que muestra las entregas realizadas, clasificado por cliente, en Buenos Aires.## Conclusiones# Segmentación y patrones en entregas.El gráfico de barras muestra cuántas entregas ha recibido cada cliente. Cada barra representa a un cliente, y su altura indica el total de entregas realizadas. Así, es fácil comparar rápidamente qué clientes tienen más o menos entregas.```{r, warning=FALSE, echo=FALSE}# Crear barchart con la cantidad total de entregas por clientebarchart_cliente <- data %>% group_by(cliente) %>% summarise(cantidad = n()) %>% ggplot(aes(x = factor(cliente), y = cantidad, fill = factor(cliente))) + # Barras con color personalizado geom_bar(stat = "identity") + # Mostrar número de entregas arriba de cada barra geom_text(aes(label = cantidad), vjust = -0.5, size = 5) + # Personalizar colores de las barras scale_fill_manual(values = c("20" = "dodgerblue", "70" = "tomato")) + # Títulos y etiquetas labs( title = "Cantidad de Entregas por Cliente", x = "Cliente", y = "Cantidad de Entregas" ) + # Tema del gráfico theme_minimal() + theme(legend.position = "none") # Ocultar leyenda si los colores coinciden con los clientes# Mostrar el gráficoprint(barchart_cliente)```Este gráfico de líneas muestra la cantidad de entregas realizadas a dos clientes específicos (identificados como "20" y "70") a lo largo de los días de la semana. Cada línea representa a un cliente, permitiendo analizar cómo varían sus entregas día a día.Podemos observar que el cliente 20 tiene un pico de entregas el miércoles y una disminución notable el jueves, con una ligera recuperación hacia el final de la semana. Por otro lado, el cliente 70 mantiene un comportamiento más estable a lo largo de los días, con pequeñas variaciones pero sin grandes picos o caídas. Esta visualización facilita comparar los patrones de entrega entre ambos clientes y puede ayudar a identificar días de mayor o menor actividad para cada uno.```{r, warning=FALSE, echo=FALSE}# Agrupar y contar la cantidad de entregas por cliente y día de la semanaentregas_por_cliente_dia <- data %>% filter(!is.na(dia_str) & dia_str != "domingo") %>% group_by(cliente, dia_str) %>% summarise(cantidad = n()) %>% ungroup() %>% mutate(dia_str = factor(dia_str, levels = c("lunes", "martes", "miércoles", "jueves", "viernes", "sábado")))# Line chart de cantidad de entregas por cliente y día de la semanalinechart_cliente_dia <- entregas_por_cliente_dia %>% ggplot(aes(x = dia_str, y = cantidad, color = factor(cliente), group = cliente)) + geom_line(size = 1) + geom_point(size = 3) + labs(title = "Cantidad de Entregas por Día de la Semana y Cliente", x = "Día de la Semana", y = "Cantidad de Entregas", color = "Cliente") + theme_minimal() + theme(legend.position = "bottom")# Mostrar el gráficoprint(linechart_cliente_dia)```Este gráfico de lineas permite la visualización de la cantidad de entregas realizadas por cliente a lo largo de los días de la semana, excluyendo el domingo. Utiliza líneas para representar la evolución del número de entregas para cada cliente, con puntos destacados en cada día.Total de ordenes con más de una entrega: 5027::: panel-tabset## Distribución```{r, warning=FALSE, echo=FALSE}entregas_por_orden <- data %>% group_by(id_orden) %>% summarize(cant_entregas = n(), has_many = cant_entregas > 1, clientes_unicos = n_distinct(cliente), .groups = "drop")# Crear el histogramaggplot(entregas_por_orden, aes(x = cant_entregas)) + geom_histogram(binwidth = 1, fill = "#94C11F", color = "#272727") + labs(title = "Histograma de Entregas por domicilio", x = "Valor", y = "Cantidad de entregas") + theme_minimal()```Este gráfico permite la visualización de la distribución de la cantidad de entregas por domicilio, mostrando un histograma que representa la frecuencia de entregas para diferentes valores.## Porcentajes```{r, warning=FALSE, echo=FALSE}# Calcular las frecuencias y porcentajesfrecuencias <- entregas_por_orden %>% count(has_many) %>% mutate(porcentaje = (n / sum(n)) * 100)# Crear el gráfico de barras con porcentajes encimaggplot(frecuencias, aes(x = has_many, y = n)) + geom_bar(stat = "identity", fill = "#94C11F", color = "#272727") + geom_text(aes(label = paste0(round(porcentaje, 1), "%")), vjust = -0.5, size = 3) + # Ajuste para que el texto aparezca encima labs(title = "Domicilios con más de una entrega", x = "¿Tiene más de una entrega?", y = "Cantidad de resultados") + theme_minimal()```Este gráfico de barra permite la visualización de la cantidad de domicilios que han recibido más de una entrega. Utiliza barras para representar el número total de domicilios, distinguiendo entre aquellos que tienen mas de una entrega y los que no.:::Este histograma muestra la distribución de la cantidad de entregas por domicilio, permitiendo identificar cuántas veces se realizaron múltiples entregas en un mismo lugar durante un periodo de tres meses. En el gráfico, cada barra representa la frecuencia de domicilios que recibieron cierta cantidad de entregas.Podemos observar que la mayoría de los domicilios recibieron dos entregas, lo que se refleja en la mayor altura de esa barra. Sin embargo, también se registran casos con más entregas, aunque en menor frecuencia, disminuyendo progresivamente a medida que aumenta el número de entregas por domicilio. En algunos casos excepcionales, se registraron más de X entregas en el mismo domicilio, lo que sugiere que ciertos clientes tienen una mayor recurrencia en los pedidos. Este análisis permite identificar tanto los patrones de entrega más comunes como los casos atípicos con entregas inusualmente altas.```{r, warning=FALSE, echo=FALSE}# Filtrar los datos de Buenos Airesbuenos_aires_data <- data %>% filter(latitud >= -35.2, latitud <= -34.3, longitud >= -58.8, longitud <= -57.9)# Extraer las coordenadascoords <- buenos_aires_data %>% dplyr::select(latitud, longitud)# Método del codo para determinar el número óptimo de clustersset.seed(123)wss_values <- sapply(1:10, function(k) { kmeans(coords, centers = k, nstart = 10)$tot.withinss})# Graficar el método del codo con ggplot2elbow_plot <- data.frame(K = 1:10, WSS = wss_values) %>% ggplot(aes(x = K, y = WSS)) + geom_point() + geom_line() + geom_vline(xintercept = 4, linetype = "dashed", color = "red") + annotate("text", x = 4.2, y = wss_values[4] + 10, label = "Elección óptima de K", color = "red", hjust = 0) + labs(title = "Método del codo para determinar K óptimo", x = "Número de Clusters K", y = "Suma de cuadrados dentro del cluster (WSS)") + theme_minimal()# Aplicar K-means con K = 4set.seed(123)kmeans_result <- kmeans(coords, centers = 4, nstart = 10)# Agregar los clusters al DataFrame originalbuenos_aires_data <- buenos_aires_data %>% mutate(cluster = as.factor(kmeans_result$cluster))```El análisis mostrado utiliza el algoritmo de **K-means** para agrupar las entregas por su **localidad y cercanía geográfica** en la ciudad de Buenos Aires. El propósito de aplicar este tipo de clustering es identificar zonas de entrega de manera más eficiente y precisa, evitando segmentaciones arbitrarias como barrios o comunas, que podrían no reflejar de manera realista los patrones de distribución.::: panel-tabset## Resultado K Medias```{r, warning=FALSE, echo=FALSE}# Definir la paleta de colores personalizada para los clusters (misma del mapa)cluster_colors <- c("1" = "#D32F2F", # Rojo "2" = "#1976D2", # Azul "3" = "#388E3C", # Verde "4" = "#7B1FA2") # Púrpura# Crear una paleta de colores para leafletpalette <- colorFactor(palette = cluster_colors, domain = buenos_aires_data$cluster)# Visualizar los clusters en un mapa interactivo con leafletleaflet(data = buenos_aires_data) %>% addTiles() %>% addCircleMarkers(~longitud, ~latitud, color = ~palette(cluster), radius = 2, fillOpacity = 0.8, popup = ~paste("Cluster:", cluster)) %>% addLegend("bottomright", pal = palette, values = ~cluster, title = "Clusters", opacity = 0.8)```Este gráfico nos ayuda a comprender el número de clusters utilizados en el análisis de K-means como la distribución geográfica de los mismos en el área de Buenos Aires.## Elbow```{r, warning=FALSE, echo=FALSE}print(elbow_plot)```Para determinar la cantidad óptima de clusters (**K**) que mejor agrupe los puntos de entrega, se aplicó el **método del codo**. Este método mide la **suma de cuadrados dentro del cluster (WSS)**, que indica cuán compactos son los grupos formados. El gráfico muestra que a medida que aumenta el número de clusters, el WSS disminuye, lo que significa que los grupos son cada vez más específicos. Sin embargo, a partir de **K = 4**, la reducción adicional en WSS es menor, formando un "codo". Por eso, seleccionamos **4 clusters** como el número óptimo para este análisis.:::### Interpretación del Mapa y los ClustersEn el mapa, los puntos de entrega están agrupados en **cuatro clusters**, cada uno representado por un color diferente. Esta división refleja de manera efectiva las zonas donde se concentran las entregas:- **Zona Norte:** Agrupa entregas hacia localidades como Vicente López y San Isidro.- **Zona Sur:** Incluye áreas como Lanús, Avellaneda y alrededores.- **Zona Oeste:** Abarca lugares como Morón y La Matanza.- **Zona Este:** Se concentra cerca de la Ciudad Autónoma de Buenos Aires, incluyendo zonas cercanas al Río de la Plata.Estos clusters permiten visualizar de forma más clara las áreas donde se realizan las entregas, facilitando la identificación de patrones geográficos importantes, como zonas con alta demanda. Esta información es útil para optimizar rutas de distribución, gestionar inventarios de forma localizada y mejorar la eficiencia operativa. Además, es posible notar que los grupos coinciden con divisiones lógicas de la ciudad (norte, sur, este y oeste), lo que respalda la validez de los clusters formados.Este enfoque basado en K-means nos permite tomar decisiones basadas en datos y adaptadas a la geografía real de las entregas, en lugar de depender de límites administrativos que podrían no reflejar los verdaderos patrones de distribución.::: panel-tabset## Entregas por zona```{r, warning=FALSE, echo=FALSE}# Calcular el número de entregas por cluster y ordenarlos de mayor a menorcluster_counts <- buenos_aires_data %>% group_by(cluster) %>% summarise(count = n(), .groups = "drop") %>% arrange(desc(count))# Definir la paleta de colores personalizada para los clusters (misma del mapa)cluster_colors <- c("1" = "#D32F2F", # Rojo "2" = "#1976D2", # Azul "3" = "#388E3C", # Verde "4" = "#7B1FA2") # Púrpura# Graficar el bar chart de cantidad de entregas por cluster con etiquetasggplot(cluster_counts, aes(x = reorder(cluster, -count), y = count, fill = cluster)) + geom_bar(stat = "identity") + geom_text(aes(label = count), vjust = -0.5, size = 5) + scale_fill_manual(values = cluster_colors) + # Aplicar paleta de colores personalizada labs(title = "Cantidad de entregas por cluster", x = "Cluster", y = "Cantidad de entregas") + theme_minimal() + theme(legend.position = "none") + ylim(0, max(cluster_counts$count) * 1.1) # Aumenta el límite superior del eje y```Este gráfico permite la visualización de cuántas entregas se han realizado en cada cluster, mostrando la cantidad de entregas en un gráfico de barras. Las barras están ordenadas de mayor a menor, lo que facilita ver rápidamente en qué clusters hay más actividad## Cobertura```{r, warning=FALSE, echo=FALSE}# Convertir el DataFrame a un objeto espacial sfbuenos_aires_sf <- buenos_aires_data %>% st_as_sf(coords = c("longitud", "latitud"), crs = 4326)# Calcular el envolvente convexo (convex hull) para cada clusterclusters_hulls <- buenos_aires_sf %>% group_by(cluster) %>% summarise(geometry = st_combine(geometry), .groups = "drop") %>% # Combina las geometrías de cada cluster st_convex_hull() # Calcula el envolvente convexo# Calcular el número de puntos en cada clustercluster_counts <- buenos_aires_data %>% group_by(cluster) %>% summarise(count = n())# Unir los conteos con los envolventes convexosclusters_hulls <- clusters_hulls %>% left_join(cluster_counts, by = "cluster")# Obtener el centroide de cada polígono de cluster para posicionar las etiquetasclusters_hulls_centroids <- st_centroid(clusters_hulls)# Graficarggplot() + # Graficar los envolventes convexos geom_sf(data = clusters_hulls, aes(fill = cluster), alpha = 0.3, color = NA) + # Graficar los puntos de datos geom_sf(data = buenos_aires_sf, aes(color = cluster), size = 0.5) + # Agregar etiquetas con el número de puntos en cada cluster geom_text(data = clusters_hulls_centroids, aes(x = st_coordinates(geometry)[,1], y = st_coordinates(geometry)[,2], label = count), size = 5, color = "black") + labs(title = "Áreas cubiertas por cada cluster en Buenos Aires", fill = "Cluster", color = "Cluster") + theme_minimal() + coord_sf()```Este mapa permite la visualización de las áreas cubiertas por cada cluster en Buenos Aires, utilizando envolventes convexos para representar la extensión de cada grupo de entregas. En él, se muestran los puntos de datos de las entregas, coloreados según su cluster, lo que facilita identificar visualmente la distribución geográfica.:::```{r, warning=FALSE, echo=FALSE}# Filtrar los días válidos (excluir "NA")deliveries_per_day_cluster <- buenos_aires_data %>% filter(dia_str != "NA") %>% # Excluir valores "NA" group_by(cluster, dia_str) %>% summarise(count = n(), .groups = "drop")# Definir la paleta de colores personalizada para los clusterscluster_colors <- c("1" = "#D32F2F", # Rojo "2" = "#1976D2", # Azul "3" = "#388E3C", # Verde "4" = "#7B1FA2") # Púrpura# Graficar el bar chart con colores personalizadosggplot(deliveries_per_day_cluster, aes(x = dia_str, y = count, fill = cluster)) + geom_bar(stat = "identity", position = "dodge") + scale_fill_manual(values = cluster_colors) + # Aplicar paleta personalizada labs(title = "Cantidad de entregas por día y por cluster", x = "Día", y = "Cantidad de entregas", fill = "Cluster") + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotar etiquetas del eje X```Este gráfico de barras muestra la **cantidad de entregas realizadas por día de la semana**, segmentadas por **clusters geográficos**. Cada barra representa el número de entregas realizadas en un día específico, y los diferentes colores identifican los clusters. Las barras están agrupadas en modo **"dodge"**, lo que facilita comparar las entregas entre clusters para cada día.#### **Patrones Observados:**- Los días con **mayor cantidad de entregas** son **martes y miércoles**, especialmente en el **cluster 3** (color verde).- El **cluster 2** (azul) tiene un comportamiento más uniforme a lo largo de la semana, sin días con picos destacados.- Los **clusters 1** (rojo) y **4** (púrpura) muestran menor volumen de entregas en comparación, pero con actividad significativa los lunes y viernes.- Los días sábado también presentan un nivel de entregas importante, pero más distribuido entre los clusters.Este gráfico ayuda a visualizar cómo se distribuyen las entregas durante la semana en las distintas zonas geográficas, permitiendo identificar **picos de actividad por día y cluster**, lo cual puede ser útil para planificar recursos logísticos y optimizar rutas de entrega.```{r, warning=FALSE, echo=FALSE}# Calcular el número de entregas por cluster y por clientedeliveries_per_cluster_cliente <- buenos_aires_data %>% group_by(cluster, cliente) %>% summarise(count = n(), .groups = "drop")# Definir la paleta de colores personalizada para los clientescliente_colors <- c("20" = "#F8766D", # Rojo claro "70" = "#00BFC4") # Azul claro# Graficar el bar chart de cantidad de entregas por cluster y por clienteggplot(deliveries_per_cluster_cliente, aes(x = cluster, y = count, fill = factor(cliente))) + geom_bar(stat = "identity", position = "dodge") + scale_fill_manual(values = cliente_colors) + # Aplicar la paleta personalizada labs(title = "Cantidad de entregas por cluster y por cliente", x = "Cluster", y = "Cantidad de entregas", fill = "Cliente") + theme_minimal()```Este gráfico de barras muestra la cantidad de entregas por **cluster geográfico** para los dos clientes, **20** y **70**. Las barras están agrupadas para facilitar la comparación entre los clientes en cada cluster.Se observa que el **cliente 70** no tiene entregas registradas en el **Cluster 1**, mientras que el cliente **20** presenta entregas en todos los clusters. El **Cluster 2** es donde se concentra el mayor número de entregas para ambos clientes, destacando un volumen particularmente alto para el cliente 20.# Análisis de series temporales```{r, warning=FALSE, echo=FALSE}# Aseguramos que la columna 'mes' tenga solo el año y mesdata_resumida <- data %>% mutate(mes = as.Date(format(mes, "%Y-%m-01")))# Agrupación de los datos por día y mesdata_resumida <- data_resumida %>% group_by(mes, dia) %>% summarize(n = n(), .groups = 'drop')# Crear una fecha combinando el día y el mesdata_resumida <- data_resumida %>% mutate(fecha = mes + days(dia - 1)) %>% arrange(fecha) # Ordenar por la fecha completa para evitar saltos# Crear el gráfico interactivo con Plotlyfig <- plot_ly(data_resumida, x = ~fecha, y = ~n, type = 'scatter', mode = 'lines+markers') %>% layout( title = "Evolución de n a lo largo del tiempo", xaxis = list( title = "Fecha", tickformat = "%d-%b", # Formato de fecha tickangle = 45 # Rotar las etiquetas del eje X ), yaxis = list(title = "n"), margin = list(b = 80) # Asegurar espacio para las etiquetas del eje X )# Mostrar el gráficofig```Tenemos 4 grandes caidas en las entregas por lo que buscamos identificar el motivo en cada fecha.| Fecha | Cantidad de entregas | Motivo ||------------------|------------------|------------------------------------|| Jueves 9 de mayo | 0 | FNTC (Camioneros) adhiere al [paro general del 9 de mayo\*](https://tn.com.ar/sociedad/2024/05/08/quienes-adhieren-al-paro-general-del-9-de-mayo-y-que-servicios-no-funcionan/). || Jueves 20 de junio | 138 | [Feriado nacional\*](https://www.argentina.gob.ar/interior/feriados-nacionales-2024) Paso a la Inmortalidad del Gral. Manuel Belgrano || **Domingo** 21 de julio | 3 | Como [explicamos anteriormente](#horarios-entrega "¿Cuales son los horarios de entrega?") , según nuestros datos iflow no realizan entregas los domingos. || 2 de agosto | 11 | Por simplicidad asumimos que se debe a falta de datos en la fecha para comparir el dataset. |Para realizar un pronóstico de demanda para el mes de agosto vamos no vamos a tomar en cuenta estos 4 casos asumiendolos como irrelevantes para el modelo. Limitando los datos hasta el 30 de julio```{r, warning=FALSE, echo=FALSE}data_resumida_clean <- data_resumida %>% filter( !(fecha == as.Date("2024-05-09")), !(fecha == as.Date("2024-06-20")), !(fecha == as.Date("2024-07-21")), !(fecha > as.Date("2024-07-30")) )# Crear el gráfico interactivo con Plotlyfig <- plot_ly(data_resumida_clean, x = ~fecha, y = ~n, type = 'scatter', mode = 'lines+markers') %>% layout( title = "Evolución de n a lo largo del tiempo", xaxis = list( title = "Fecha", tickformat = "%d-%b", # Formato de fecha tickangle = 45 # Rotar las etiquetas del eje X ), yaxis = list(title = "n"), margin = list(b = 80) # Asegurar espacio para las etiquetas del eje X )# Mostrar el gráficofig```Con los datos ordenados ahora podemos descomponer la serie temporal en tendencia, estacionalidad y residuales.```{r, warning=FALSE, echo=FALSE}# Convertir los datos a serie temporal (suponiendo frecuencia diaria)ts_data <- ts(data_resumida_clean$n, frequency = 7) # Frecuencia semanal# Descomposición de la serie temporaldescomposicion <- stl(ts_data, s.window = "periodic")# Gráfico de la descomposiciónplot(descomposicion)```Con esta **descomposición de la serie temporal**, podemos analizar los siguientes aspectos clave:1. **Serie original (Data):** Nos muestra los valores reales a lo largo del tiempo, permitiendo visualizar los cambios y fluctuaciones en los datos.2. **Componente estacional (Seasonal):** Identifica los patrones repetitivos que ocurren en intervalos regulares, como ciclos mensuales o semanales. Este componente es útil para detectar estacionalidad, lo que puede ayudar a prever comportamientos futuros basados en estos ciclos.3. **Tendencia (Trend):** Nos indica si los datos siguen una dirección general creciente, decreciente o estable a lo largo del tiempo. La tendencia es esencial para evaluar el comportamiento a largo plazo y detectar cambios estructurales.4. **Residuo (Remainder):** Muestra las variaciones o anomalías que no se explican por la estacionalidad ni la tendencia. Este componente permite identificar eventos atípicos o ruido aleatorio que afecta los datos.Esta descomposición permite entender los **patrones subyacentes** de la serie temporal, facilitando un análisis más preciso. Podemos, por ejemplo, diferenciar entre fluctuaciones temporales (estacionales) y cambios estructurales (tendencias), y evaluar la magnitud del **ruido o error** para mejorar modelos de predicción.```{r, warning=FALSE, echo=FALSE}# Filtrar los datos para entrenamiento y validacióntrain_data <- data_resumida_clean %>% filter(fecha >= as.Date("2024-05-01") & fecha <= as.Date("2024-06-30"))test_data <- data_resumida_clean %>% filter(fecha >= as.Date("2024-07-01") & fecha <= as.Date("2024-07-31"))# Convertir los datos de entrenamiento y validación a series temporalestrain_ts <- ts(train_data$n, start = c(2024, 5), frequency = 7)test_ts <- ts(test_data$n, start = c(2024, 7), frequency = 7)# Ajustar los modelos ARIMA(1,0,1) y ARIMA(1,1,1)modelo_arima_101 <- Arima(train_ts, order = c(1,0,1))print(modelo_arima_101)modelo_arima_111 <- Arima(train_ts, order = c(1,1,1))print(modelo_arima_111)# Predecir para el mismo número de pasos que los datos de pruebapred_101 <- forecast(modelo_arima_101, h = length(test_ts))pred_111 <- forecast(modelo_arima_111, h = length(test_ts))# Asegurar que las predicciones y los datos de prueba tengan la misma longitudif (length(test_ts) == length(pred_101$mean)) { # Reemplazar valores NA en predicciones, si los hubiera pred_101$mean[is.na(pred_101$mean)] <- 0 pred_111$mean[is.na(pred_111$mean)] <- 0 # Alinear las predicciones y los datos de prueba aligned_pred_101 <- ts(pred_101$mean, start = start(test_ts), frequency = 7) aligned_pred_111 <- ts(pred_111$mean, start = start(test_ts), frequency = 7) # Calcular errores para ARIMA(1,0,1) MAE_101 <- mean(abs(test_ts - aligned_pred_101), na.rm = TRUE) RMSE_101 <- sqrt(mean((test_ts - aligned_pred_101)^2, na.rm = TRUE)) # Calcular errores para ARIMA(1,1,1) MAE_111 <- mean(abs(test_ts - aligned_pred_111), na.rm = TRUE) RMSE_111 <- sqrt(mean((test_ts - aligned_pred_111)^2, na.rm = TRUE)) # Mostrar resultados cat("Errores ARIMA(1,0,1):\n MAE:", MAE_101, "\n RMSE:", RMSE_101, "\n") cat("Errores ARIMA(1,1,1):\n MAE:", MAE_111, "\n RMSE:", RMSE_111, "\n")} else { cat("Error: Las longitudes de las predicciones y los datos de prueba no coinciden.\n")}```# Unidades de TransporteEn los datos proporcionados no contamos con la información necesaria para realizar un análisis específico de las unidades de transporte, ya que sería indispensable disponer de un identificador único para cada vehículo. Sin embargo, incluimos esta sección como prueba de concepto para demostrar el valor que podría generar este tipo de análisis en la operación logística. Disponer de esta información permitiría evaluar aspectos fundamentales de la gestión de la flota, optimización de rutas y eficiencia operativa.Enumeramos algunos análisis posibles con la información de los camiones, unidades de transporte.1. **Optimización de Rutas:** Identificar las rutas más eficientes y reducir viajes innecesarios para minimizar costos y tiempo de entrega.```{=html}<!-- -->```1. **Utilización de la Flota:** Evaluar el uso real de cada vehículo para detectar capacidad ociosa y ajustar la cantidad de unidades necesarias.2. **Desempeño Operativo:** Medir tiempos activos (entregas) versus tiempos inactivos (espera o mantenimiento) para mejorar la eficiencia y planificación.Algunos analisis que realizamos como prueba de concepto..Primero identificamos la ubicacion de las sedes de Iflow y las comparamos con la primera entrega de cada día para entender si existe algún patron con respecto al orden de las entregas.::: panel-tabset## Sedes vs Primeras entregas```{r, warning=FALSE, echo=FALSE}# Definir las coordenadas en la variable 'sedes'sedes <- data.frame( lat = c(-34.4574168, -34.4312098, -34.4616837, -34.4616837), lon = c(-58.7347338, -58.7254967, -58.7390698, -58.7390698), zoom = c(14, NA, NA, 17) # Columna opcional para definir niveles de zoom)# Suponiendo que 'data' tiene las columnas necesarias: 'fin_visita', 'latitud', 'longitud', 'cliente'primeras_entregas <- data %>% mutate(fecha = as.Date(fin_visita)) %>% group_by(fecha) %>% filter(fin_visita == min(fin_visita)) %>% ungroup()# Crear un mapa interactivo con Leafletmapa_leaflet <- leaflet() %>% addTiles() %>% # Agregar marcadores para las primeras entregas en azul addCircleMarkers( data = primeras_entregas, ~longitud, ~latitud, radius = 3, color = "blue", fillOpacity = 0.8, popup = ~paste("Fecha:", fecha, "<br>Cliente:", cliente, "<br>Hora:", fin_visita) ) %>% # Agregar marcadores para las sedes en rojo addCircleMarkers( data = sedes, ~lon, ~lat, radius = 5, color = "red", fillOpacity = 1, popup = ~paste("Sede") ) %>% # Agregar la leyenda addLegend( "bottomright", colors = c("blue", "red"), labels = c("Primera Entrega", "Sede"), title = "Leyenda" )# Mostrar el mapamapa_leaflet```## Sedes de Iflow```{r, warning=FALSE, echo=FALSE}# Crear el mapa interactivo usando Plotly y Mapboxfig <- plot_ly( data = sedes, type = 'scattermapbox', # Define el tipo de gráfico lat = ~lat, lon = ~lon, mode = 'markers', # Establece los puntos como marcadores marker = list(size = 10, color = 'red') # Opcional: personalización de marcadores)# Añadir el estilo de Mapboxfig <- fig %>% layout( mapbox = list( style = 'open-street-map', # Puedes usar otros estilos como 'streets', 'satellite' zoom = 10, # Nivel de zoom inicial center = list(lat = mean(sedes$lat), lon = mean(sedes$lon)) # Centrar el mapa ) )# Mostrar el gráficofig```:::Luego graficamos la primera y segunda entrega de cada día. Esperaríamos que estos valores se encuentren en pares, muy cercanos entre si.```{r, warning=FALSE, echo=FALSE}# Encontrar la primera y la segunda entrega de cada díaentregas_dia <- data %>% mutate(fecha = as.Date(fin_visita)) %>% group_by(fecha) %>% arrange(fin_visita) %>% # Ordenar por hora de finalización slice(1:2) %>% # Seleccionar las dos primeras entregas mutate(tipo = ifelse(row_number() == 1, "Primera Entrega", "Segunda Entrega")) %>% ungroup()# Crear el mapa interactivo con Leafletmapa_leaflet <- leaflet(data = entregas_dia) %>% addTiles() %>% addCircleMarkers( data = entregas_dia %>% filter(tipo == "Primera Entrega"), ~longitud, ~latitud, radius = 3, color = "blue", fillOpacity = 0.8, popup = ~paste("Fecha:", fecha, "<br>Cliente:", cliente, "<br>Hora:", fin_visita) ) %>% addCircleMarkers( data = entregas_dia %>% filter(tipo == "Segunda Entrega"), ~longitud, ~latitud, radius = 5, color = "red", fillOpacity = 0.8, popup = ~paste("Fecha:", fecha, "<br>Cliente:", cliente, "<br>Hora:", fin_visita) ) %>% addLegend( "bottomright", colors = c("blue", "red"), labels = c("Primera Entrega", "Segunda Entrega"), title = "Leyenda" )# Mostrar el mapamapa_leaflet```En este gráfico en el cual visualizamos los puntos de la primer y segunda entrega del día, encontramos cercanías entre los puntos azules (1er entrega del día) respecto a los puntos rojos (2da entrega del día) lo cual nos hace suponer que realizan las entregas de acuerdo a la cercanía con una ubicación específica.```{r, warning=FALSE, echo=FALSE}# Parámetros: Selección de fecha específica y cantidad de entregas fecha_seleccionada <- as.Date("2024-05-23") n_entregas <- 20```Luego, tomando como referencia la fecha 2024-05-23, graficamos las primeras 20 entregas de ese día y las conectamos en orden cronológico.::: panel-tabset## Ocultar tiempo entre entregas```{r, warning=FALSE, echo=FALSE}# Filtrar las entregas para la fecha seleccionada y ordenarlas por horaentregas_fecha <- data %>% filter(as.Date(fin_visita) == fecha_seleccionada) %>% arrange(fin_visita) %>% slice(1:n_entregas) %>% mutate( orden = row_number(), # Agregar el orden de entrega tiempo_transcurrido = c(NA, diff(fin_visita) / 60) # Calcular minutos entre entregas )# Crear la paleta de colores basada en el valor del clientepaleta_colores <- colorFactor( palette = c("red", "green"), # Asignar colores específicos domain = entregas_fecha$cliente # Basado en los valores de la columna cliente)# Crear etiquetas de tiempo entre entregas para colocarlas en las líneasetiquetas_lineas <- entregas_fecha %>% filter(!is.na(tiempo_transcurrido)) %>% # Excluir la primera entrega (sin tiempo anterior) mutate( lat_medio = (latitud + lag(latitud)) / 2, lng_medio = (longitud + lag(longitud)) / 2, etiqueta_tiempo = paste0(round(tiempo_transcurrido, 2), " min") ) %>% na.omit() # Remover filas con NA (la primera fila)# Crear el mapa interactivo con Leafletmapa_leaflet <- leaflet(data = entregas_fecha) %>% addTiles() %>% # Agregar puntos con colores basados en el cliente y etiquetas personalizadas addCircleMarkers( ~longitud, ~latitud, radius = 8, color = "black", fillColor = ~paleta_colores(cliente), fillOpacity = 0.8, label = ~as.character(orden), # Mostrar el número de orden como etiqueta labelOptions = labelOptions( noHide = TRUE, direction = "center", textOnly = TRUE, style = list( "font-size" = "13px", "font-weight" = "bold", "color" = "white", "border-radius" = "100%", "padding" = "12px" ) ), # Mostrar la hora exacta y tiempo transcurrido como popup popup = ~paste( "Hora de Entrega:", format(fin_visita, "%H:%M:%S"), "<br>Cliente:", cliente, "<br>Tiempo desde la última entrega:", ifelse(is.na(tiempo_transcurrido), "N/A", paste(round(tiempo_transcurrido, 2), "min")) ) ) %>% # Agregar líneas que conecten las entregas en orden addPolylines( lng = ~longitud, lat = ~latitud, data = entregas_fecha, color = "blue", weight = 2 ) %>% # Agregar una leyenda para los clientes addLegend( "bottomright", pal = paleta_colores, values = ~cliente, title = "Cliente", opacity = 1 )# Mostrar el mapamapa_leaflet```## Mostrar tiempo entre entregas```{r, warning=FALSE, echo=FALSE}# Filtrar las entregas para la fecha seleccionada y ordenarlas por horaentregas_fecha <- data %>% filter(as.Date(fin_visita) == fecha_seleccionada) %>% arrange(fin_visita) %>% slice(1:n_entregas) %>% mutate( orden = row_number(), # Agregar el orden de entrega tiempo_transcurrido = c(NA, diff(fin_visita) / 60) # Calcular minutos entre entregas )# Crear la paleta de colores basada en el valor del clientepaleta_colores <- colorFactor( palette = c("red", "green"), # Asignar colores específicos domain = entregas_fecha$cliente # Basado en los valores de la columna cliente)# Crear etiquetas de tiempo entre entregas para colocarlas en las líneasetiquetas_lineas <- entregas_fecha %>% filter(!is.na(tiempo_transcurrido)) %>% # Excluir la primera entrega (sin tiempo anterior) mutate( lat_medio = (latitud + lag(latitud)) / 2, lng_medio = (longitud + lag(longitud)) / 2, etiqueta_tiempo = paste0(round(tiempo_transcurrido, 2), " min") ) %>% na.omit() # Remover filas con NA (la primera fila)# Crear el mapa interactivo con Leafletmapa_leaflet <- leaflet(data = entregas_fecha) %>% addTiles() %>% # Agregar puntos con colores basados en el cliente y etiquetas personalizadas addCircleMarkers( ~longitud, ~latitud, radius = 8, color = "black", fillColor = ~paleta_colores(cliente), fillOpacity = 0.8, label = ~as.character(orden), # Mostrar el número de orden como etiqueta labelOptions = labelOptions( noHide = TRUE, direction = "center", textOnly = TRUE, style = list( "font-size" = "13px", "font-weight" = "bold", "color" = "white", "border-radius" = "100%", "padding" = "12px" ) ), # Mostrar la hora exacta y tiempo transcurrido como popup popup = ~paste( "Hora de Entrega:", format(fin_visita, "%H:%M:%S"), "<br>Cliente:", cliente, "<br>Tiempo desde la última entrega:", ifelse(is.na(tiempo_transcurrido), "N/A", paste(round(tiempo_transcurrido, 2), "min")) ) ) %>% # Agregar líneas que conecten las entregas en orden addPolylines( lng = ~longitud, lat = ~latitud, data = entregas_fecha, color = "blue", weight = 2 ) %>% # Agregar etiquetas sobre las líneas con tiempo transcurrido addLabelOnlyMarkers( lng = ~lng_medio, lat = ~lat_medio, label = ~etiqueta_tiempo, data = etiquetas_lineas, labelOptions = labelOptions( noHide = TRUE, direction = "top", style = list( "font-size" = "10px", "font-weight" = "bold", "color" = "blue" ) ) ) %>% # Agregar una leyenda para los clientes addLegend( "bottomright", pal = paleta_colores, values = ~cliente, title = "Cliente", opacity = 1 )# Mostrar el mapamapa_leaflet```:::Esto nos permite identificar algunos patrones.Los camiones no llevan productos de distintos clientes, lleva o productos del cliente 20 o del 70.Existen distintos camiones que podrian ser identificados gracias a las grandes distancias entre entregas con muy poco tiempo de diferencia.En donde se realizan cruces de lineas podemos identificar rutas mal optimizadas.## Intentos de identificar transportistas.A priori y para facilitar el análisis podríamos suponer que las entregas las realizan dos camiones. Uno para cada cliente. Podemos validar que esto es incorrecto al estudiar las entregas de un único cliente.```{r, warning=FALSE, echo=FALSE}# Filtrar las entregas para la fecha seleccionada y ordenarlas por horaentregas_fecha <- data %>% filter(as.Date(fin_visita) == fecha_seleccionada, cliente == 20) %>% arrange(fin_visita) %>% slice(1:n_entregas) %>% mutate( orden = row_number(), # Agregar el orden de entrega tiempo_transcurrido = c(NA, diff(fin_visita) / 60) # Calcular minutos entre entregas )# Crear etiquetas de tiempo entre entregas para colocarlas en las líneasetiquetas_lineas <- entregas_fecha %>% filter(!is.na(tiempo_transcurrido)) %>% # Excluir la primera entrega (sin tiempo anterior) mutate( lat_medio = (latitud + lag(latitud)) / 2, lng_medio = (longitud + lag(longitud)) / 2, etiqueta_tiempo = paste0(round(tiempo_transcurrido, 2), " min") ) %>% na.omit() # Remover filas con NA (la primera fila)# Crear el mapa interactivo con Leafletmapa_leaflet <- leaflet(data = entregas_fecha) %>% addTiles() %>% # Agregar puntos con colores basados en el cliente y etiquetas personalizadas addCircleMarkers( ~longitud, ~latitud, radius = 8, color = "red", label = ~as.character(orden), # Mostrar el número de orden como etiqueta labelOptions = labelOptions( noHide = TRUE, direction = "center", textOnly = TRUE, style = list( "font-size" = "13px", "font-weight" = "bold", "color" = "white", "border-radius" = "100%", "padding" = "12px" ) ), # Mostrar la hora exacta y tiempo transcurrido como popup popup = ~paste( "Hora de Entrega:", format(fin_visita, "%H:%M:%S"), "<br>Cliente:", cliente, "<br>Tiempo desde la última entrega:", ifelse(is.na(tiempo_transcurrido), "N/A", paste(round(tiempo_transcurrido, 2), "min")) ) ) %>% # Agregar líneas que conecten las entregas en orden addPolylines( lng = ~longitud, lat = ~latitud, data = entregas_fecha, color = "blue", weight = 2 ) %>% # Agregar una leyenda para los clientes addLegend( "bottomright", pal = paleta_colores, values = ~cliente, title = "Cliente", opacity = 1 )# Mostrar el mapamapa_leaflet```Vemos que entre la entrega 18 y 19 pasaron menos de 3minHay algunos casos donde podemos identificar camiones distintos:- Grandes distancias en poco tiempo.- Cruces de rutas.Los cruces de trayectorias podrían indicar rutas ineficientes de los repartidores o que se estan siguiendo varios repartidores en la misma ruta. Podemos verlo de forma clara en el siguiente gráfico:```{r echo=FALSE}# Filtrar las entregas para la fecha seleccionada y ordenarlas por horaentregas_fecha <- data %>% filter(as.Date(fin_visita) == fecha_seleccionada, cliente == 70) %>% arrange(fin_visita) %>% slice(1:n_entregas) %>% mutate( orden = row_number(), # Agregar el orden de entrega tiempo_transcurrido = c(NA, diff(fin_visita) / 60) # Calcular minutos entre entregas )# Crear etiquetas de tiempo entre entregas para colocarlas en las líneasetiquetas_lineas <- entregas_fecha %>% filter(!is.na(tiempo_transcurrido)) %>% # Excluir la primera entrega (sin tiempo anterior) mutate( lat_medio = (latitud + lag(latitud)) / 2, lng_medio = (longitud + lag(longitud)) / 2, etiqueta_tiempo = paste0(round(tiempo_transcurrido, 2), " min") ) %>% na.omit() # Remover filas con NA (la primera fila)# Crear el mapa interactivo con Leafletmapa_leaflet <- leaflet(data = entregas_fecha) %>% addTiles() %>% # Agregar puntos con colores basados en el cliente y etiquetas personalizadas addCircleMarkers( ~longitud, ~latitud, radius = 8, color = "blue", label = ~as.character(orden), # Mostrar el número de orden como etiqueta labelOptions = labelOptions( noHide = TRUE, direction = "center", textOnly = TRUE, style = list( "font-size" = "13px", "font-weight" = "bold", "color" = "white", "border-radius" = "100%", "padding" = "12px" ) ), # Mostrar la hora exacta y tiempo transcurrido como popup popup = ~paste( "Hora de Entrega:", format(fin_visita, "%H:%M:%S"), "<br>Cliente:", cliente, "<br>Tiempo desde la última entrega:", ifelse(is.na(tiempo_transcurrido), "N/A", paste(round(tiempo_transcurrido, 2), "min")) ) ) %>% # Agregar líneas que conecten las entregas en orden addPolylines( lng = ~longitud, lat = ~latitud, data = entregas_fecha, color = "blue", weight = 2 ) %>% # Agregar una leyenda para los clientes addLegend( "bottomright", pal = paleta_colores, values = ~cliente, title = "Cliente", opacity = 1 )# Mostrar el mapamapa_leaflet```Habiendo identificado estas caracteristicas podríamos utilizar un modelo de agrupamiento para estimar distintos repartidores y sus trayectorias.Esto tiene un gran valor para:1. Identificar rutas ineficientes.2. Estimar un tiempo de duración de cada entrega (Y rellenar vacios)3. Identificar patrones en los errores de carga de datos (Duración de entregas)Para la aplicación practica de estos analisis sería vital tener acceso real a los camiones o repartidores responsables de cada entrega pero para el bien de este analisis intentamos aproximar lo más posible a estos datos utilizando modelos estadisticos. Esperamos sirva como prueba de concepto para evaluar el potencial de recolectar estos datos.Esto nos presenta un problema que podría solucionarse con coloración de grafos.**Construir el grafo de conflictos:**- **Nodos:** Cada nodo representa una entrega individual con su respectiva latitud, longitud, fecha y hora.- **Aristas (conflictos):** Se dibuja una arista entre dos entregas si es imposible que hayan sido realizadas por el mismo repartidor debido a restricciones de tiempo y distancia. - **Criterio de conflicto:** Para dos entregas AAA y BBB, se calcula el tiempo mínimo necesario para que un repartidor viaje desde la ubicación de AAA a BBB considerando una velocidad razonable (por ejemplo, la velocidad promedio de un vehículo en esa área). - Si el tiempo transcurrido entre la hora de entrega de AAA y BBB es menor que el tiempo mínimo de viaje calculado, entonces se establece un conflicto entre AAA y BBB.**Algoritmos sugeridos:**- **Algoritmo Greedy de coloreo:** Un enfoque sencillo que asigna colores a los nodos de forma secuencial, utilizando el menor número de colores posible en cada paso.- **Heurísticas avanzadas:** Si el grafo es grande y complejo, pueden emplearse algoritmos como DSATUR o técnicas metaheurísticas (algoritmos genéticos, recocido simulado) para aproximar una solución cercana al mínimo número de colores.Algunos resultados obtenidos utilizando el algoritmo Greedy de coloreo con python:Si bien funciona correctamente para alrededor de 20 entregas a medida que agregamos datos se dificulta el algoritmo y considerando las incosistencias de los datos y errores de carga no sería adecuado estimar usando este algoritmo en su estado actual.Algunas consideraciones.- Toma en cuenta una velocidad promedio de los camiones arbitrairaSi bien podría ser interesante utilizar un algoritmo para resolver o estimar el problema. Evidentmente la mejor solución es tomar los datos de UNIGIS o un software automatico. De todas formas no se podrá extraer valor real con datos de tiempo erroneos.Esta estimación nos presenta algunas preguntas sobre factores de las entregas.1. Rutas optimizadas. Gracias al análisis podemos ver que las rutas estan optimizadas pero parecen estar diseñadas para transportistas individuales y no para la flota como grupo. Optimizar las rutas de entregas para toda la flota como un ente es distinto a diseñar la ruta para cada camion entre dos puntos.# ApéndiceEnlace al repositorio de github <https://github.com/LorennMarque/iflow>